Repository navigation
Expand file tree
/
Copy pathtools_c.h
More file actions
1157 lines (1064 loc) · 41.9 KB
/
Copy pathtools_c.h
File metadata and controls
1157 lines (1064 loc) · 41.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Read / Write / Edit: the three self-improvement file tools, adapted from
// ~/coder (crates/coder-tools/src/cc/{read,write,edit}.rs) into plain C so the
// Bend FFI layer (sys_c.c) wraps one implementation — the agent loop itself
// lives in bendcoder_agent.bend.
//
// Every entry point returns a malloc'd string the caller frees. Failures come
// back as readable text prefixed with "error: " rather than as a status code,
// because that text is fed straight back into the agent's state for the next
// Classify round.
#ifndef BENDCODER_TOOLS_C_H
#define BENDCODER_TOOLS_C_H
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
// MAX_OUTPUT_SIZE from ~/coder files.rs: the whole-file size a Read refuses
// once no limit narrows it, so a stray read cannot swamp the agent's state.
#define BENDCODER_MAX_OUTPUT_SIZE (256 * 1024)
// How much of a line the nearest-match hint shows when an Edit misses.
#define BENDCODER_NEAREST_MAX_LINE 200
// ----------------------------------------------------------------------------
// Shared helpers
// ----------------------------------------------------------------------------
static char* bendcoder_fmt(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
#include <stdarg.h>
// Allocates exactly as much as the formatted message needs, so an error can
// quote an arbitrarily long old_string without being clipped.
static char* bendcoder_fmt(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
va_list ap2;
va_copy(ap2, ap);
int n = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (n < 0) {
va_end(ap2);
return strdup("error: message formatting failed");
}
char* out = (char*)malloc((size_t)n + 1);
if (!out) {
va_end(ap2);
return strdup("error: out of memory");
}
vsnprintf(out, (size_t)n + 1, fmt, ap2);
va_end(ap2);
return out;
}
// Reads the whole file. Returns NULL when it cannot be opened; on success
// *out_len holds the byte count and the buffer is NUL-terminated.
static char* bendcoder_slurp(const char* path, size_t* out_len) {
FILE* f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz < 0) { fclose(f); return NULL; }
char* buf = (char*)malloc((size_t)sz + 1);
if (!buf) { fclose(f); return NULL; }
size_t r = fread(buf, 1, (size_t)sz, f);
buf[r] = '\0';
fclose(f);
if (out_len) *out_len = r;
return buf;
}
static int bendcoder_is_dir(const char* path) {
struct stat st;
if (stat(path, &st) != 0) return 0;
return S_ISDIR(st.st_mode);
}
static int bendcoder_exists(const char* path) {
struct stat st;
return stat(path, &st) == 0;
}
// `mkdir -p` over the path's parent directories, so Write and Edit can create
// a file in a directory that does not exist yet.
static int bendcoder_mkdir_parents(const char* path) {
char* copy = strdup(path);
if (!copy) return -1;
int rc = 0;
for (char* p = copy + 1; *p; p++) {
if (*p != '/') continue;
*p = '\0';
if (!bendcoder_exists(copy) && mkdir(copy, 0755) != 0) { rc = -1; }
*p = '/';
if (rc != 0) break;
}
free(copy);
return rc;
}
static size_t bendcoder_count_matches(const char* haystack, const char* needle) {
size_t needle_len = strlen(needle);
if (needle_len == 0) return 0;
size_t count = 0;
const char* p = haystack;
while ((p = strstr(p, needle)) != NULL) {
count++;
p += needle_len;
}
return count;
}
// Replaces the first `max` occurrences of `old` with `new` (max == 0 replaces
// every occurrence). *out_len receives the result's byte length so a content
// with embedded NULs still writes back at the right size.
static char* bendcoder_replace(const char* content, size_t content_len,
const char* old_str, const char* new_str,
size_t max, size_t* out_len) {
size_t old_len = strlen(old_str);
size_t new_len = strlen(new_str);
size_t hits = bendcoder_count_matches(content, old_str);
if (max > 0 && hits > max) hits = max;
size_t cap = content_len + hits * new_len + 1;
if (new_len < old_len) cap = content_len + 1;
char* out = (char*)malloc(cap);
if (!out) return NULL;
size_t o = 0;
size_t done = 0;
const char* cur = content;
const char* end = content + content_len;
while (cur < end) {
const char* hit = (max == 0 || done < max) ? strstr(cur, old_str) : NULL;
if (!hit) {
size_t tail = (size_t)(end - cur);
memcpy(out + o, cur, tail);
o += tail;
break;
}
size_t prefix = (size_t)(hit - cur);
memcpy(out + o, cur, prefix);
o += prefix;
memcpy(out + o, new_str, new_len);
o += new_len;
cur = hit + old_len;
done++;
}
out[o] = '\0';
if (out_len) *out_len = o;
return out;
}
static char* bendcoder_write_bytes(const char* path, const char* data, size_t len) {
bendcoder_mkdir_parents(path);
FILE* f = fopen(path, "wb");
if (!f) return bendcoder_fmt("error: Failed to open %s for writing", path);
size_t w = fwrite(data, 1, len, f);
fclose(f);
if (w != len) return bendcoder_fmt("error: Short write to %s (%zu of %zu bytes)", path, w, len);
return NULL;
}
// ----------------------------------------------------------------------------
// Read
// ----------------------------------------------------------------------------
// tool_read_io is the IO half of Read — the only part that must live in C.
// The directory and existence checks, the whole-file size cap, the slurp and
// the BOM strip all need the file's size or its bytes. Everything downstream
// (the split on '\n', the offset/limit selection, the "N\tline" rendering) is
// a pure String -> String that lives in tool_read.bend, where its laws are
// proved; tool_read below is that pure half's C mirror, kept so test_tools.c
// can exercise the rendering the Bend half's laws cover. Keep them in step.
//
// Returns a malloc'd buffer. *code is 0 on success and *len the byte count
// after the BOM strip; on failure *code is an errno-style category and the
// buffer holds the "error: " text tool_read reported.
static char* tool_read_io(const char* path, long limit, int* code, size_t* len) {
if (bendcoder_is_dir(path)) {
*code = EISDIR;
char* msg = bendcoder_fmt("error: EISDIR: illegal operation on a directory, read '%s'", path);
*len = msg ? strlen(msg) : 0;
return msg;
}
struct stat st;
if (stat(path, &st) != 0) {
*code = ENOENT;
char* msg = bendcoder_fmt("error: File does not exist: %s", path);
*len = msg ? strlen(msg) : 0;
return msg;
}
if (limit <= 0 && st.st_size > BENDCODER_MAX_OUTPUT_SIZE) {
*code = EFBIG;
char* msg = bendcoder_fmt(
"error: File content (%lld bytes) exceeds maximum allowed size (%d bytes). "
"Use offset and limit to read specific portions of the file.",
(long long)st.st_size, BENDCODER_MAX_OUTPUT_SIZE);
*len = msg ? strlen(msg) : 0;
return msg;
}
char* content = bendcoder_slurp(path, len);
if (!content) {
*code = EIO;
char* msg = bendcoder_fmt("error: Failed to read %s", path);
*len = msg ? strlen(msg) : 0;
return msg;
}
// Skip a UTF-8 BOM in place, so the returned buffer stays the allocation
// and the FFI worker can free it without tracking a second pointer.
if (*len >= 3 && (unsigned char)content[0] == 0xEF &&
(unsigned char)content[1] == 0xBB && (unsigned char)content[2] == 0xBF) {
memmove(content, content + 3, *len - 2);
*len -= 3;
}
*code = 0;
return content;
}
// The C mirror of render() in tool_read.bend — 1-indexed lines rendered as
// "N\tline", the format ~/coder's add_line_numbers produces, so a line number
// quoted back by the model addresses the same line. `offset` is the first
// line to emit (0 and 1 both mean line 1); `limit` <= 0 means "to the end of
// the file". test_tools.c is its caller; sys_c.c uses tool_read_io.
__attribute__((unused))
static char* tool_read(const char* path, long offset, long limit) {
if (offset < 1) offset = 1;
int code = 0;
size_t len = 0;
char* text = tool_read_io(path, limit, &code, &len);
if (code) return text;
if (len == 0) {
free(text);
return strdup("<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>");
}
// Split on '\n' the way the source algorithm does: a trailing newline yields
// a final empty line, so "a\n" counts as two lines.
long first = offset - 1;
long last = (limit > 0) ? first + limit : -1;
size_t cap = len + 64;
char* out = (char*)malloc(cap);
if (!out) { free(text); return strdup("error: out of memory"); }
size_t o = 0;
long index = 0;
long emitted = 0;
char* cur = text;
char* stop = text + len;
while (cur <= stop) {
char* nl = memchr(cur, '\n', (size_t)(stop - cur));
char* line_end = nl ? nl : stop;
if (index >= first && (last < 0 || index < last)) {
size_t line_len = (size_t)(line_end - cur);
if (line_len > 0 && cur[line_len - 1] == '\r') line_len--; // CRLF
// 24 covers the line number, the tab and the newline.
if (o + line_len + 24 > cap) {
cap = (o + line_len + 64) * 2;
char* grown = (char*)realloc(out, cap);
if (!grown) { free(out); free(text); return strdup("error: out of memory"); }
out = grown;
}
if (emitted > 0) out[o++] = '\n';
o += (size_t)snprintf(out + o, cap - o, "%ld\t", index + 1);
memcpy(out + o, cur, line_len);
o += line_len;
emitted++;
}
index++;
if (!nl) break;
cur = nl + 1;
}
out[o] = '\0';
free(text);
if (emitted == 0) {
free(out);
return bendcoder_fmt(
"<system-reminder>Warning: the file exists but is shorter than the provided offset (%ld). "
"The file has %ld lines.</system-reminder>", offset, index);
}
return out;
}
// ----------------------------------------------------------------------------
// Write
// ----------------------------------------------------------------------------
// Full write / overwrite, creating any missing parent directories.
static char* tool_write(const char* path, const char* content, size_t len) {
if (bendcoder_is_dir(path)) {
return bendcoder_fmt("error: EISDIR: illegal operation on a directory, write '%s'", path);
}
int existed = bendcoder_exists(path);
char* err = bendcoder_write_bytes(path, content, len);
if (err) return err;
return existed
? bendcoder_fmt("The file %s has been updated successfully.", path)
: bendcoder_fmt("File created successfully at: %s", path);
}
// A model that has just read a file through Read sees "N\tline" and routinely
// quotes that numbering back when it writes an old_string, dropping the number
// but keeping the tab. Strips a leading line-number prefix from each line so
// such an old_string can still be matched against the file. Only ever used as
// a fallback after the exact string fails, and only kept when it resolves.
static char* bendcoder_strip_line_prefixes(const char* s, int* changed) {
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
size_t o = 0;
int any = 0;
size_t i = 0;
while (i <= len) {
// Measure this line.
size_t line_end = i;
while (line_end < len && s[line_end] != '\n') line_end++;
size_t j = i;
size_t digits = 0;
while (j < line_end && s[j] >= '0' && s[j] <= '9') { j++; digits++; }
if (digits > 0 && j < line_end && (s[j] == '\t' || s[j] == ':')) {
i = j + 1; // "26\t" or "26:"
any = 1;
} else if (i < line_end && s[i] == '\t') {
i = i + 1; // the number already dropped, the tab left behind
any = 1;
}
size_t keep = line_end - i;
memcpy(out + o, s + i, keep);
o += keep;
if (line_end < len) out[o++] = '\n';
i = line_end + 1;
if (line_end >= len) break;
}
out[o] = '\0';
if (changed) *changed = any;
return out;
}
// The common leading whitespace run shared by every non-empty line of s —
// zero when the text is not uniformly indented. A model that quotes the file
// inside an indented block sends old_string with a uniform indent the file
// does not have, which is what this measures for the fallback below.
static size_t bendcoder_common_indent(const char* s) {
size_t common = (size_t)-1;
size_t len = strlen(s);
size_t i = 0;
while (i < len) {
size_t line_end = i;
while (line_end < len && s[line_end] != '\n') line_end++;
size_t ws = 0;
while (i + ws < line_end && (s[i + ws] == ' ' || s[i + ws] == '\t')) ws++;
if (i + ws < line_end && ws < common) common = ws; // a non-empty line bounds the run
i = line_end + 1;
}
return common == (size_t)-1 ? 0 : common;
}
// Strip up to `width` leading space/tab characters from each line — the
// companion of common_indent. Lines with less whitespace keep their content.
static char* bendcoder_dedent(const char* s, size_t width) {
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
size_t o = 0;
size_t i = 0;
while (i <= len) {
size_t line_end = i;
while (line_end < len && s[line_end] != '\n') line_end++;
size_t ws = 0;
while (ws < width && i + ws < line_end && (s[i + ws] == ' ' || s[i + ws] == '\t')) ws++;
size_t keep = line_end - i - ws;
memcpy(out + o, s + i + ws, keep);
o += keep;
if (line_end < len) out[o++] = '\n';
i = line_end + 1;
if (line_end >= len) break;
}
out[o] = '\0';
return out;
}
// A new_string whose lines open with a hunk-number prefix — "N\t" or
// "N.N\t" — is a numbered diff hunk (or a Read page) pasted as content, not
// the file's text: a #41 draft wrote "87\t"-numbered lines into run_tests.sh
// itself (#47). old_string recovers from the same paste by stripping, but only
// because the stripped spelling is verified against the file before use;
// new_string has nothing to verify a strip against, and a partially-numbered
// paste would land half-stripped, so any hunk-numbered line refuses the edit
// outright. Returns the refusal text, or NULL when no line is numbered. A
// trailing newline's empty tail is not a line — the phantom rule grep_scan
// uses — so "1\tx\n" counts as one numbered line, not a line and a bare one.
static char* bendcoder_hunk_refusal(const char* new_str) {
size_t len = strlen(new_str);
size_t pre = 0, all = 0;
size_t i = 0;
while (i < len) {
size_t line_end = i;
while (line_end < len && new_str[line_end] != '\n') line_end++;
all++;
size_t j = i;
while (j < line_end && new_str[j] >= '0' && new_str[j] <= '9') j++;
if (j > i && j < line_end && new_str[j] == '\t') {
pre++; // "N<tab>"
} else if (j > i && j < line_end && new_str[j] == '.') {
size_t k = j + 1;
while (k < line_end && new_str[k] >= '0' && new_str[k] <= '9') k++;
if (k > j + 1 && k < line_end && new_str[k] == '\t') pre++; // "N.N<tab>"
}
i = line_end + 1;
}
if (pre == 0) return NULL;
return bendcoder_fmt(
"error: new_string is a numbered diff hunk, not file content — %zu of its "
"%zu lines carry an \"N<tab>\" hunk-number prefix. Re-draft the "
"replacement as plain file content, without the line numbers.", pre, all);
}
// When old_string does not match, "not found" is a dead end: it says what did
// not happen, not what is true, so a model with a wrong idea of the file has
// nothing to correct against and proposes the same text again. This locates the
// longest leading run of old_string that does occur and renders the real lines
// there, turning the failure into a correction signal.
static char* bendcoder_nearest_hint(const char* content, const char* old_str) {
// Every line of old_string is tried as an anchor, not just the first. When a
// model fabricates, it usually invents around something real — a made-up
// comment above a genuine line of code — so the first line is often the least
// reliable one to search for.
const char* hit = NULL;
const char* line = old_str;
while (*line && !hit) {
const char* line_nl = strchr(line, '\n');
const char* line_stop = line_nl ? line_nl : line + strlen(line);
const char* start = line;
while (start < line_stop && (*start == ' ' || *start == '\t')) start++;
size_t line_len = (size_t)(line_stop - start);
while (line_len > 0 && (start[line_len - 1] == ' ' || start[line_len - 1] == '\r')) line_len--;
if (line_len >= 8) {
char* needle = (char*)malloc(line_len + 1);
if (!needle) return NULL;
memcpy(needle, start, line_len);
needle[line_len] = '\0';
// Shrink from the right, so a line that is nearly right still anchors on
// the part that is.
for (size_t len = line_len; len >= 8; len--) {
needle[len] = '\0';
hit = strstr(content, needle);
if (hit) break;
}
free(needle);
}
if (!line_nl) break;
line = line_nl + 1;
}
if (!hit) return NULL;
// Which line is it on, and what do the next few lines actually say?
long line_no = 1;
for (const char* p = content; p < hit; p++) {
if (*p == '\n') line_no++;
}
const char* line_start = hit;
while (line_start > content && line_start[-1] != '\n') line_start--;
// Four lines, each clipped, is all the context that is useful here, so a
// fixed buffer is enough and keeps this independent of the Grep section.
char out[4 * (BENDCODER_NEAREST_MAX_LINE + 24)];
size_t o = 0;
const char* p = line_start;
for (int i = 0; i < 4 && *p && o < sizeof(out) - 1; i++) {
const char* e = strchr(p, '\n');
size_t l = e ? (size_t)(e - p) : strlen(p);
if (l > BENDCODER_NEAREST_MAX_LINE) l = BENDCODER_NEAREST_MAX_LINE;
int n = snprintf(out + o, sizeof(out) - o, "%ld:%.*s\n", line_no + i, (int)l, p);
if (n < 0) break;
o += (size_t)n < sizeof(out) - o ? (size_t)n : sizeof(out) - o - 1;
if (!e) break;
p = e + 1;
}
if (o > 0 && out[o - 1] == '\n') out[o - 1] = '\0';
return strdup(out);
}
// ----------------------------------------------------------------------------
// Edit
// ----------------------------------------------------------------------------
// Exact-match search and replace. An ambiguous `old_string` is refused rather
// than guessed at: with more than one match and replace_all off, the caller is
// told how many matches there are and asked for more context.
static char* tool_edit(const char* path, const char* old_str, const char* new_str,
int replace_all) {
if (strcmp(old_str, new_str) == 0) {
return strdup("error: `old_string` and `new_string` are identical; nothing to replace.");
}
// A numbered new_string is refused before anything else — the paste class
// fires on the create path too, where old_string is empty and no fallback
// would ever look at the replacement text.
char* hunk_err = bendcoder_hunk_refusal(new_str);
if (hunk_err) return hunk_err;
int existed = bendcoder_exists(path);
// An empty old_string means "create this file", matching ~/coder's Edit.
if (old_str[0] == '\0') {
if (existed) {
return bendcoder_fmt("error: Cannot create new file - file already exists: %s", path);
}
char* err = bendcoder_write_bytes(path, new_str, strlen(new_str));
if (err) return err;
return bendcoder_fmt("File created successfully at: %s", path);
}
if (!existed) return bendcoder_fmt("error: File does not exist: %s", path);
if (bendcoder_is_dir(path)) {
return bendcoder_fmt("error: EISDIR: illegal operation on a directory, edit '%s'", path);
}
size_t len = 0;
char* content = bendcoder_slurp(path, &len);
if (!content) return bendcoder_fmt("error: Failed to read %s", path);
size_t matches = bendcoder_count_matches(content, old_str);
// The exact string is authoritative. Only when it is absent is the
// line-number-stripped spelling tried, and only if that one resolves.
char* stripped = NULL;
if (matches == 0) {
int changed = 0;
stripped = bendcoder_strip_line_prefixes(old_str, &changed);
if (stripped && changed) {
size_t stripped_matches = bendcoder_count_matches(content, stripped);
if (stripped_matches > 0) {
old_str = stripped;
matches = stripped_matches;
}
}
}
// A model that quotes the file inside an indented block sends an
// old_string uniformly indented past the file's real text; the dedented
// spelling is tried the same way, and when it resolves the new_string is
// dedented by the same amount so the replacement keeps the file's
// indentation rather than the model's quoting indent.
char* dedented = NULL;
char* dedented_new = NULL;
if (matches == 0) {
size_t width = bendcoder_common_indent(old_str);
if (width > 0) {
dedented = bendcoder_dedent(old_str, width);
if (dedented) {
size_t dedented_matches = bendcoder_count_matches(content, dedented);
if (dedented_matches > 0) {
old_str = dedented;
matches = dedented_matches;
dedented_new = bendcoder_dedent(new_str, width);
if (dedented_new) {
new_str = dedented_new;
// Dedenting can expose hunk numbers the quoting indent hid, so
// the paste check runs again on the transformed replacement.
char* derr = bendcoder_hunk_refusal(new_str);
if (derr) {
free(stripped);
free(dedented);
free(dedented_new);
free(content);
return derr;
}
}
}
}
}
}
if (matches == 0) {
char* hint = bendcoder_nearest_hint(content, old_str);
char* err;
if (hint) {
err = bendcoder_fmt(
"error: String to replace not found in %s. The closest text found is:\n%s\n"
"Copy from there exactly, without the \"N<tab>\" prefix Read adds.\n"
"Your string was:\n%s", path, hint, old_str);
free(hint);
} else {
err = bendcoder_fmt(
"error: String to replace not found in %s, and no part of it appears in "
"the file at all — re-read the file before editing it.\nYour string was:\n%s",
path, old_str);
}
free(stripped);
free(dedented);
free(dedented_new);
free(content);
return err;
}
if (matches > 1 && !replace_all) {
char* err = bendcoder_fmt(
"error: Found %zu matches of the string to replace in %s, but replace_all is false. "
"To replace all occurrences set replace_all to true. To replace one occurrence, "
"provide more context to uniquely identify the instance.\nString: %s",
matches, path, old_str);
free(stripped);
free(dedented);
free(dedented_new);
free(content);
return err;
}
// Deleting a line: when new_string is empty and old_string has no trailing
// newline, the newline after the match goes with it, so no blank line is left
// behind.
char* search = (char*)old_str;
char* search_owned = NULL;
size_t old_len = strlen(old_str);
if (new_str[0] == '\0' && old_len > 0 && old_str[old_len - 1] != '\n') {
search_owned = (char*)malloc(old_len + 2);
if (search_owned) {
memcpy(search_owned, old_str, old_len);
search_owned[old_len] = '\n';
search_owned[old_len + 1] = '\0';
if (strstr(content, search_owned)) {
search = search_owned;
} else {
free(search_owned);
search_owned = NULL;
}
}
}
size_t updated_len = 0;
char* updated = bendcoder_replace(content, len, search, new_str,
replace_all ? 0 : 1, &updated_len);
free(content);
free(search_owned);
free(stripped);
if (!updated) {
free(dedented);
free(dedented_new);
return strdup("error: out of memory");
}
char* err = bendcoder_write_bytes(path, updated, updated_len);
free(updated);
free(dedented);
free(dedented_new);
if (err) return err;
return replace_all
? bendcoder_fmt("The file %s has been updated. All %zu occurrences were successfully replaced.", path, matches)
: bendcoder_fmt("The file %s has been updated successfully.", path);
}
// ----------------------------------------------------------------------------
// Grep
// ----------------------------------------------------------------------------
// Literal (not regular-expression) search. A file is searched on its own and
// matches render as "N:line"; a directory is searched recursively and matches
// render as "path:N:line", the spelling grep itself uses, so a result can be
// handed straight to Read or Edit.
// Matches are capped on both counts because the result goes into the agent's
// state: a common pattern over a whole tree would otherwise crowd out
// everything else the agent had learned.
#define BENDCODER_GREP_MAX_MATCHES 200
#define BENDCODER_GREP_MAX_LINE 500
#define BENDCODER_GREP_MAX_FILE_SIZE (1024 * 1024)
// Lines shown on either side of a hit, like grep -C. A bare "path:N:line"
// says where a match is but nothing about what contains it — which function,
// which struct — and the model invented the rest.
#define BENDCODER_GREP_CONTEXT 3
// The shortest piece of a missed pattern worth reporting (shorter pieces
// match everywhere and say nothing), how many file names the report lists,
// and the room kept for them.
#define BENDCODER_FRAG_MIN 4
#define BENDCODER_FRAG_MAX_NAMES 6
#define BENDCODER_FRAG_NAMES_CAP 768
typedef struct {
char* data;
size_t len;
size_t cap;
size_t matches;
int truncated;
int failed;
} GrepSink;
static void grep_sink_put(GrepSink* g, const char* text, size_t len) {
if (g->failed) return;
if (g->len + len + 1 > g->cap) {
size_t cap = (g->len + len + 1) * 2;
char* grown = (char*)realloc(g->data, cap);
if (!grown) { g->failed = 1; return; }
g->data = grown;
g->cap = cap;
}
memcpy(g->data + g->len, text, len);
g->len += len;
g->data[g->len] = '\0';
}
static void grep_sink_fmt(GrepSink* g, const char* fmt, ...) {
if (g->failed) return;
va_list ap;
va_start(ap, fmt);
char buf[BENDCODER_GREP_MAX_LINE + 512];
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n > 0) grep_sink_put(g, buf, (size_t)n < sizeof(buf) ? (size_t)n : sizeof(buf) - 1);
}
// The line-level test. The first pass is an exact substring match; the retry
// a miss earns is case-insensitive and walks the line a byte at a time.
static int line_has(const char* line, const char* pattern, int ci) {
if (!ci) return strstr(line, pattern) != NULL;
size_t plen = strlen(pattern);
for (const char* p = line; *p; p++) {
if (strncasecmp(p, pattern, plen) == 0) return 1;
}
return 0;
}
// One output line. Matches use ':' ("path:N:line"), context uses '-'
// ("path-N-line"), the convention grep -C prints.
static void grep_emit(GrepSink* g, const char* label, size_t line_no,
const char* text, size_t line_len, char sep) {
size_t shown = line_len > BENDCODER_GREP_MAX_LINE ? BENDCODER_GREP_MAX_LINE : line_len;
const char* clip = shown < line_len ? " ..." : "";
if (label) {
grep_sink_fmt(g, "%s%c%zu%c%.*s%s\n", label, sep, line_no, sep,
(int)shown, text, clip);
} else {
grep_sink_fmt(g, "%zu%c%.*s%s\n", line_no, sep, (int)shown, text, clip);
}
}
// Scans one file's bytes. `label` prefixes each line when searching a tree,
// and is NULL for a single-file search. Hits carry BENDCODER_GREP_CONTEXT lines
// on either side, with a "--" between blocks that do not touch.
static void grep_scan(GrepSink* g, const char* pattern, const char* label,
char* text, size_t len, int ci) {
// The ring holds the last few lines not yet emitted, so a hit can show the
// lines just above it.
size_t ring_off[BENDCODER_GREP_CONTEXT];
size_t ring_len[BENDCODER_GREP_CONTEXT];
int ring_n = 0;
long emitted = 0; // highest line number written so far
int after = 0; // trailing context lines still owed to the last hit
size_t line_no = 1;
size_t i = 0;
while (i <= len) {
if (g->truncated || g->failed) return;
size_t line_end = i;
while (line_end < len && text[line_end] != '\n') line_end++;
size_t line_len = line_end - i;
if (line_len > 0 && text[i + line_len - 1] == '\r') line_len--; // CRLF
// The position after a final newline is a phantom line, not a real one:
// it can never match, and as context it would read as a stray blank.
if (line_len == 0 && line_end >= len) break;
// The line is NUL-terminated in place for the search and put back after,
// which keeps the scan allocation-free.
char saved = text[i + line_len];
text[i + line_len] = '\0';
int hit = line_has(text + i, pattern, ci);
text[i + line_len] = saved;
if (hit) {
if (g->matches >= BENDCODER_GREP_MAX_MATCHES) {
g->truncated = 1;
return;
}
// Leading context comes out of the ring; lines already shown stay shown,
// and a gap between blocks is marked the way grep marks it.
long ctx_from = (long)line_no - ring_n;
if (ctx_from < emitted + 1) ctx_from = emitted + 1;
long first_out = ctx_from < (long)line_no ? ctx_from : (long)line_no;
if (emitted > 0 && first_out > emitted + 1) grep_sink_put(g, "--\n", 3);
for (int k = 0; k < ring_n; k++) {
long ctx_no = (long)line_no - ring_n + k;
if (ctx_no < ctx_from) continue;
grep_emit(g, label, (size_t)ctx_no, text + ring_off[k], ring_len[k], '-');
}
grep_emit(g, label, line_no, text + i, line_len, ':');
g->matches++;
emitted = (long)line_no;
after = BENDCODER_GREP_CONTEXT;
ring_n = 0;
} else if (after > 0) {
grep_emit(g, label, line_no, text + i, line_len, '-');
emitted = (long)line_no;
after--;
} else {
if (ring_n == BENDCODER_GREP_CONTEXT) {
memmove(ring_off, ring_off + 1, sizeof(ring_off[0]) * (BENDCODER_GREP_CONTEXT - 1));
memmove(ring_len, ring_len + 1, sizeof(ring_len[0]) * (BENDCODER_GREP_CONTEXT - 1));
ring_n--;
}
ring_off[ring_n] = i;
ring_len[ring_n] = line_len;
ring_n++;
}
line_no++;
if (line_end >= len) break;
i = line_end + 1;
}
}
// Binary files have no lines worth showing and would corrupt the state, so a
// NUL byte near the head is taken as the signal to skip the file.
static int grep_looks_binary(const char* text, size_t len) {
size_t check = len < 8192 ? len : 8192;
return memchr(text, '\0', check) != NULL;
}
static void grep_file(GrepSink* g, const char* pattern, const char* path,
const char* label, int ci) {
struct stat st;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) return;
if (st.st_size > BENDCODER_GREP_MAX_FILE_SIZE) return;
size_t len = 0;
char* text = bendcoder_slurp(path, &len);
if (!text) return;
if (!grep_looks_binary(text, len)) grep_scan(g, pattern, label, text, len, ci);
free(text);
}
// Tracked files only, the way ripgrep searches by default. A working directory
// holds build output that git ignores but a tree walk does not: here, 450 KB of
// generated C that is larger than the entire source tree and full of plausible
// matches. `git ls-files` answers this authoritatively; outside a repository
// the set stays empty and everything is searched, which is the right fallback.
static char* tracked_files = NULL;
static void tracked_files_load(void) {
if (tracked_files) return;
FILE* p = popen("git ls-files 2>/dev/null", "r");
if (!p) { tracked_files = strdup(""); return; }
size_t cap = 8192, len = 0;
char* buf = (char*)malloc(cap);
if (!buf) { pclose(p); tracked_files = strdup(""); return; }
buf[0] = '\n';
len = 1;
char line[1024];
while (fgets(line, sizeof(line), p)) {
size_t l = strlen(line);
if (len + l + 2 > cap) {
cap = (len + l + 2) * 2;
char* grown = (char*)realloc(buf, cap);
if (!grown) break;
buf = grown;
}
memcpy(buf + len, line, l);
len += l;
if (line[l - 1] != '\n') buf[len++] = '\n';
}
pclose(p);
buf[len] = '\0';
tracked_files = buf;
}
// Empty set means "not a repository": search everything rather than nothing.
static int is_tracked(const char* path) {
tracked_files_load();
if (!tracked_files || tracked_files[1] == '\0') return 1;
char needle[1200];
snprintf(needle, sizeof(needle), "\n%s\n", path);
return strstr(tracked_files, needle) != NULL;
}
// `tracked_only` is set when the search root is a relative path, i.e. inside
// this repository. An absolute path is somewhere else entirely and the
// repository's tracked set says nothing useful about it.
static void grep_tree(GrepSink* g, const char* pattern, const char* dir,
int tracked_only, int ci) {
if (g->truncated || g->failed) return;
DIR* d = opendir(dir);
if (!d) return;
struct dirent* entry;
while ((entry = readdir(d)) != NULL) {
if (entry->d_name[0] == '.') continue; // . .. .git and other dot entries
char child[1024];
// "./x" reads worse than "x" in a result a model will quote back.
if (strcmp(dir, ".") == 0) {
snprintf(child, sizeof(child), "%s", entry->d_name);
} else {
snprintf(child, sizeof(child), "%s/%s", dir, entry->d_name);
}
if (bendcoder_is_dir(child)) {
grep_tree(g, pattern, child, tracked_only, ci);
} else if (!tracked_only || is_tracked(child)) {
grep_file(g, pattern, child, child, ci);
}
if (g->truncated || g->failed) break;
}
closedir(d);
}
// One pass over the search scope: a file on its own, or the tree under a
// directory.
static void grep_run(GrepSink* g, const char* pattern, const char* path, int ci) {
if (bendcoder_is_dir(path)) {
grep_tree(g, pattern, path, path[0] != '/', ci);
} else {
grep_file(g, pattern, path, NULL, ci);
}
}
// ----------------------------------------------------------------------------
// What a miss should say
// ----------------------------------------------------------------------------
// "No matches" alone is a dead end: the model guessed a name, guessed wrong,
// and learned nothing about which part of the guess was real, so it guesses
// again in the same shape. A miss therefore earns a case-insensitive retry,
// and if that finds nothing either, the longest prefix and suffix of the
// pattern that DO appear are reported along with where, so the next guess
// starts from a piece of something real.
// Substring search over counted bytes — the needles are pieces of the
// pattern, not NUL-terminated strings of their own. Case-insensitive, like
// the retry that runs just before this.
static const char* bendcoder_memmem(const char* hay, size_t haylen,
const char* needle, size_t nlen) {
if (nlen == 0) return hay;
if (nlen > haylen) return NULL;
for (size_t i = 0; i + nlen <= haylen; i++) {
if (strncasecmp(hay + i, needle, nlen) == 0) return hay + i;
}
return NULL;
}
typedef struct {
const char* pat;
size_t plen;
size_t best_pre, best_suf; // longest prefix/suffix of pat seen anywhere
size_t pre_files, suf_files; // how many files contain them
int pre_named, suf_named; // how many of those names were kept
char pre_names[BENDCODER_FRAG_NAMES_CAP];
char suf_names[BENDCODER_FRAG_NAMES_CAP];
} FragScan;
// The longest edge-piece of the pattern present in a buffer. Presence is
// monotone — if a piece of length k occurs, every shorter piece along the
// same edge occurs inside it — so binary search needs only a few probes.
static size_t frag_longest(const char* text, size_t len, const char* pat,
size_t plen, int suffix) {
size_t lo = 0, hi = plen - 1; // the whole pattern already missed
while (lo < hi) {
size_t mid = lo + (hi - lo + 1) / 2;
const char* needle = suffix ? pat + plen - mid : pat;
if (bendcoder_memmem(text, len, needle, mid)) lo = mid; else hi = mid - 1;
}
return lo;
}
// Files are counted only against the longest piece: any file containing the
// best fragment has it as its own longest, so the counts stay exact as the
// best grows.
static void frag_count(FragScan* fs, int suffix, size_t k, const char* path) {
size_t* best = suffix ? &fs->best_suf : &fs->best_pre;
size_t* files = suffix ? &fs->suf_files : &fs->pre_files;
int* named = suffix ? &fs->suf_named : &fs->pre_named;
char* names = suffix ? fs->suf_names : fs->pre_names;
if (k == 0 || k < *best) return;
if (k > *best) {
*best = k;
*files = 0;
*named = 0;
names[0] = '\0';
}
(*files)++;
if (*named < BENDCODER_FRAG_MAX_NAMES) {
size_t used = strlen(names);
snprintf(names + used, BENDCODER_FRAG_NAMES_CAP - used, "%s%s",
used ? ", " : "", path);
(*named)++;
}
}
static void frag_scan_file(FragScan* fs, const char* path) {
struct stat st;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) return;