This is a lightly edited transcript of my first read through the code, which is conveniently in a single file. However, that’s 2619 lines long, so I didn’t read the code line by line. I started from the top, and I got a sense of what was there. Here I’m trying to go from the what (the contents of the build on disk) to the why, trying to guess or remember why the code is what it is, as a starting point for any further work on it.
README.md
bpa.c
bpa.md
functions.sh
prompts
sample.pgn
sample1.png
sample2.png
spanio_orig
The README.md file describes the project in specific details, how it processes games using stockfish, and how it can be installed and used. The file bpa.c is the main build, and bpa.md exists to describe the source code and eventually to replace it. In functions.sh are some hints as to the development workflow as the code was written. The sample.pgn file contains a single sample game and was used for manual testing during development. In sample1.png and sample2.png are some chess positions as rendered by lichess from the processed outputs. The spanio_orig is a trace of the development history but can be deleted or ignored.
The complete directory listing is more than the build, which actually includes only the bpa.c file itself. The build command is gcc -o bpa bpa.c as documented in the README.
The first 20 lines of the file chess_bpa/bpa.c.
1 /* chess_bpa */
2
3 #define _GNU_SOURCE // for memmem
4 #include <stdlib.h>
5 #include <stdio.h>
6 #include <assert.h>
7 #include <string.h>
8 #include <stdarg.h>
9 #include <sys/types.h>
10 #include <sys/stat.h>
11 #include <fcntl.h>
12 #include <unistd.h>
13 #include <sys/wait.h>
14 #include <ctype.h>
15 #include <limits.h>
16 /* convenient debugging macros */
17 #define dbgd(x) prt(#x ": %d\n", x),flush()
18 #define dbgx(x) prt(#x ": %x\n", x),flush()
19 #define dbgf(x) prt(#x ": %f\n", x),flush()
20
Line 1 is a header, this comment style provided context cues to the LLM even though it only saw the entire file contents every time.
Next we have a define that changes the C dialect slightly so that we can use memmem. Then we have 11 includes: stdlib, stdio, assert, string, stdarg, sys/types, sys/stat, fcntl, unistd, sys/wait, ctype, limits. Line 16 is another header, announcing the convenient debugging macros dbgd, dbgx, dbgf which respectively print in decimal, hex, or float and are used for quick and dirty debugging.
In the next 500 lines we get into library functions. We will skip this part, because it is not interesting as part of this program. However, since it is the only library code, it is inlined in the file and so we include it verbatim here.
21 #define exit2(x) { flush(); exit(x); }
22
23 typedef unsigned char u8;
24
25 /* span
26
27 Think of span as our string type.
28 A span is two pointers, one to the first char included in the string, and the second to the first char excluded after the string's end.
29 If these two pointers are equal, the string is empty, but it still points to a location.
30 So two empty spans are not necessarily the same span, while two empty strings are.
31 Neither spans nor their contents are immutable.
32 These two pointers must point into some space that has been allocated somewhere.
33 Usually the spans are backed by the input buffer or the output buffer or a scratch buffer that we create.
34 A scratch buffer allocated for a particular phase of processing can be seen as a form of arena allocation.
35 A common pattern is for(;s.buf<s.end;s.buf++) { ... s.buf[0] ... } or similar.
36 */
37
38 typedef struct {
39 u8 *buf;
40 u8 *end;
41 } span;
42
43 #define BUF_SZ (1 << 30)
44
45 u8 *input_space; // remains immutable once stdin has been read up to EOF.
46 u8 *output_space;
47 u8 *cmp_space;
48 span out, inp, cmp;
49 /*
50 The inp variable is the span which writes into input_space, and then is the immutable copy of stdin for the duration of the process.
51 The number of bytes of input is len(inp).
52 */
53
54 int empty(span);
55 int len(span);
56
57 void init_spans(); // init buffers
58
59 /*
60 The output is stored in span out, which points to output_space.
61 Input processing is generally by reading out of the inp span or subspans of it.
62 The output spans are mostly written to with prt() and other IO functions.
63 The cmp_space and cmp span which points to it are used for analysis and model data, both reading and writing.
64 */
65
66 void prt2cmp();
67 void prt2std();
68 void prt(const char *, ...);
69 void w_char(char);
70 void wrs(span);
71 void bksp();
72 void sp();
73 void terpri();
74 void flush();
75 void flush_err();
76 void flush_to(char*);
77 void redir(span);
78 span reset();
79 void w_char_esc(char);
80 void w_char_esc_pad(char);
81 void w_char_esc_dq(char);
82 void w_char_esc_sq(char);
83 void wrs_esc();
84 void save();
85 void push(span);
86 void pop(span*);
87 void advance1(span*);
88 void advance(span*,int);
89 int find_char(span s, char c);
90 span pop_into_span();
91 span take_n(int, span*);
92 span first_n(span, int);
93 int span_eq(span, span);
94 int span_cmp(span, span);
95 span S(char*);
96 span nullspan();
97
98 /* our input statistics on raw bytes */
99
100 int counts[256] = {0};
101
102 void read_and_count_stdin(); // populate inp and counts[]
103 int empty(span s) {
104 return s.end == s.buf;
105 }
106
107 int len(span s) { return s.end - s.buf; }
108
109 u8 in(span s, u8* p) { return s.buf <= p && p < s.end; }
110
111 int out_WRITTEN = 0, cmp_WRITTEN = 0;
112
113 void init_spans() {
114 input_space = malloc(BUF_SZ);
115 output_space = malloc(BUF_SZ);
116 cmp_space = malloc(BUF_SZ);
117 out.buf = output_space;
118 out.end = output_space;
119 inp.buf = input_space;
120 inp.end = input_space + BUF_SZ;
121 cmp.buf = cmp_space;
122 cmp.end = cmp_space;
123 }
124
125 void bksp() {
126 out.end -= 1;
127 }
128
129 void sp() {
130 w_char(' ');
131 }
132
133 /* we might have a generic take() which would take from inp */
134 /* we might have the same kind of redir_i() as we have redir() already, where we redirect input to come from a span and then use standard functions like take() and get rid of these special cases for taking input from streams or spans. */
135
136 /* take_n is a mutating function which takes the first n chars of the span into a new span, and also modifies the input span to remove this same prefix.
137 After a function call such as `span new = take_n(x, s)`, it will be the case that `new` contatenated with `s` is equivalent to `s` before the call.
138 */
139
140 span take_n(int n, span *io) {
141 span ret;
142 ret.buf = io->buf;
143 ret.end = io->buf + n;
144 io->buf += n;
145 return ret;
146 }
147
148 /*int span_eq(span a, span b) {
149 if (len(a) != len(b)) return 0;
150 while (a.buf < a.end) {
151 if (*a.buf != *b.buf) return 0;
152 a.buf++;
153 b.buf++;
154 }
155 return 1;
156 }
157 */
158
159 int span_eq(span s1, span s2) {
160 if (len(s1) != len(s2)) return 0;
161 for (int i = 0; i < len(s1); ++i) if (s1.buf[i] != s2.buf[i]) return 0;
162 return 1;
163 }
164
165 span S(char *s) {
166 span ret = {(u8*)s, (u8*)s + strlen(s) };
167 return ret;
168 }
169
170 void read_and_count_stdin() {
171 int c;
172 while ((c = getchar()) != EOF) {
173 //if (c == ' ') continue;
174 assert(c != 0);
175 counts[c]++;
176 *inp.buf = c;
177 inp.buf++;
178 if (len(inp) == BUF_SZ) exit2(1);
179 }
180 inp.end = inp.buf;
181 inp.buf = input_space;
182 }
183
184 span saved_out[16] = {0};
185 int saved_out_stack = 0;
186
187 void redir(span new_out) {
188 assert(saved_out_stack < 15);
189 saved_out[saved_out_stack++] = out;
190 out = new_out;
191 }
192
193 span reset() {
194 assert(saved_out_stack);
195 span ret = out;
196 out = saved_out[--saved_out_stack];
197 return ret;
198 }
199
200 // set if debugging some crash
201 const int ALWAYS_FLUSH = 0;
202
203 void swapcmp() { span swap = cmp; cmp = out; out = swap; int swpn = cmp_WRITTEN; cmp_WRITTEN = out_WRITTEN; out_WRITTEN = swpn; }
204 void prt2cmp() { /*if (out.buf == output_space)*/ swapcmp(); }
205 void prt2std() { /*if (out.buf == cmp_space)*/ swapcmp(); }
206
207 void prt(const char * fmt, ...) {
208 va_list ap;
209 va_start(ap, fmt);
210 out.end += vsprintf((char*)out.end, fmt, ap);
211 if (out.buf + BUF_SZ < out.end) {
212 printf("OUTPUT OVERFLOW (%ld)\n", out.end - output_space);
213 exit2(7);
214 }
215 va_end(ap);
216 if (ALWAYS_FLUSH) flush();
217 }
218
219 void terpri() {
220 *out.end = '\n';
221 out.end++;
222 if (ALWAYS_FLUSH) flush();
223 }
224
225 void w_char(char c) {
226 *out.end++ = c;
227 }
228
229 void w_char_esc(char c) {
230 if (c < 0x20 || c == 127) {
231 out.end += sprintf((char*)out.end, "\\%03o", (u8)c);
232 } else {
233 *out.end++ = c;
234 }
235 }
236
237 void w_char_esc_pad(char c) {
238 if (c < 0x20 || c == 127) {
239 out.end += sprintf((char*)out.end, "\\%03o", (u8)c);
240 } else {
241 sp();sp();sp();
242 *out.end++ = c;
243 }
244 }
245
246 void w_char_esc_dq(char c) {
247 if (c < 0x20 || c == 127) {
248 out.end += sprintf((char*)out.end, "\\%03o", (u8)c);
249 } else if (c == '"') {
250 *out.end++ = '\\';
251 *out.end++ = '"';
252 } else if (c == '\\') {
253 *out.end++ = '\\';
254 *out.end++ = '\\';
255 } else {
256 *out.end++ = c;
257 }
258 }
259
260 void w_char_esc_sq(char c) {
261 if (c < 0x20 || c == 127) {
262 out.end += sprintf((char*)out.end, "\\%03o", (u8)c);
263 } else if (c == '\'') {
264 *out.end++ = '\\';
265 *out.end++ = '\'';
266 } else if (c == '\\') {
267 *out.end++ = '\\';
268 *out.end++ = '\\';
269 } else {
270 *out.end++ = c;
271 }
272 }
273
274 void wrs(span s) {
275 for (u8 *c = s.buf; c < s.end; c++) w_char(*c);
276 }
277
278 void wrs_esc(span s) {
279 for (u8 *c = s.buf; c < s.end; c++) w_char_esc(*c);
280 }
281
282 // flush() is used to send our out buffer (written to by prt) to stdout.
283 void flush() {
284 if (out_WRITTEN < len(out)) {
285 printf("%.*s", len(out) - out_WRITTEN, out.buf + out_WRITTEN);
286 out_WRITTEN = len(out);
287 fflush(stdout);
288 }
289 }
290
291 void flush_err() {
292 if (out_WRITTEN < len(out)) {
293 fprintf(stderr, "%.*s", len(out) - out_WRITTEN, out.buf + out_WRITTEN);
294 out_WRITTEN = len(out);
295 fflush(stderr);
296 }
297 }
298
299 void flush_to(char *fname) {
300 int fd = open(fname, O_CREAT | O_WRONLY | O_TRUNC, 0666);
301 dprintf(fd, "%*s", len(out) - out_WRITTEN, out.buf + out_WRITTEN);
302 //out_WRITTEN = len(out);
303 // reset for constant memory usage
304 out_WRITTEN = 0;
305 out.end = out.buf;
306 //fsync(fd);
307 close(fd);
308 }
309
310 u8 *save_stack[16] = {0};
311 int save_count = 0;
312
313 void save() {
314 push(out);
315 }
316
317 span pop_into_span() {
318 span ret;
319 ret.buf = save_stack[--save_count];
320 ret.end = out.end;
321 return ret;
322 }
323
324 void push(span s) {
325 save_stack[save_count++] = s.buf;
326 }
327
328 void pop(span *s) {
329 s->buf = save_stack[--save_count];
330 }
331
332 void advance1(span *s) {
333 if (!empty(*s)) s->buf++;
334 }
335
336 void advance(span *s, int n) {
337 if (len(*s) >= n) s->buf += n;
338 else s->buf = s->end; // Move to the end if n exceeds span length
339 }
340
341 // new code to copy back
342 int span_cmp(span s1, span s2) {
343 for (;;) {
344 if (empty(s1) && !empty(s2)) return 1;
345 if (empty(s2) && !empty(s1)) return -1;
346 if (empty(s1)) return 0;
347 int dif = *(s1.buf++) - *(s2.buf++);
348 if (dif) return dif;
349 }
350 }
351
352 int contains(span, span);
353
354 int contains(span haystack, span needle) {
355 /*
356 prt("contains() haystack:\n");
357 wrs(haystack);terpri();
358 prt("needle:\n");
359 wrs(needle);terpri();
360 */
361 if (len(haystack) < len(needle)) {
362 return 0; // Needle is longer, so it cannot be contained
363 }
364 void *result = memmem(haystack.buf, haystack.end - haystack.buf, needle.buf, needle.end - needle.buf);
365 return result != NULL ? 1 : 0;
366 }
367
368 span first_n(span s, int n) {
369 span ret;
370 if (len(s) < n) n = len(s); // Ensure we do not exceed the span's length
371 ret.buf = s.buf;
372 ret.end = s.buf + n;
373 return ret;
374 }
375
376 int find_char(span s, char c) {
377 for (int i = 0; i < len(s); ++i) {
378 if (s.buf[i] == c) return i;
379 }
380 return -1; // Character not found
381 }
382
383 span next_line(span*);
384
385 /* next_line(span*) shortens the input span and returns the first line as a new span.
386 The newline is consumed and is not part of either the returned span or the input span after the call.
387 I.e. the total len of the shortened input and the returned line is one less than the len of the original input.
388 If there is no newline found, then the entire input is returned.
389 In this case the input span is mutated such that buf now points to end.
390 This makes it an empty span and thus a null span in our nomenclature, but it is still an empty span at a particular location.
391 This convention of empty but localized spans allows us to perform comparisons without needing to handle them differently in the case of an empty span.
392 */
393
394 span next_line(span *input) {
395 if (empty(*input)) return nullspan();
396 span line;
397 line.buf = input->buf;
398 while (input->buf < input->end && *input->buf != '\n') {
399 input->buf++;
400 }
401 line.end = input->buf;
402 if (input->buf < input->end) { // If '\n' found, move past it for next call
403 input->buf++;
404 }
405 return line;
406 }
407
408 /*
409 In consume_prefix(span*,span) we are given a span which is typically something being parsed and another span which is expected to be a prefix of it.
410 If the prefix is found, we return 1 and modify the span that is being parsed to remove the prefix.
411 Otherwise we leave that span unmodified and return 0.
412 Typical use is in an if statement to either identify and consume some prefix and then continue on to handle what follows it, or otherwise to skip the if and continue parsing the unmodified input.
413 */
414
415 int consume_prefix(span *input, span prefix) {
416 if (len(*input) < len(prefix) || !span_eq(first_n(*input, len(prefix)), prefix)) {
417 return 0; // Prefix not found or input shorter than prefix
418 }
419 input->buf += len(prefix); // Remove prefix by advancing the start
420 return 1;
421 }
422
423 typedef struct {
424 span *s; // array of spans (points into span arena)
425 int n; // length of array
426 } spans;
427
428 #define SPAN_ARENA_STACK 256
429
430 span* span_arena;
431 int span_arenasz;
432 int span_arena_used;
433 int span_arena_stack[SPAN_ARENA_STACK];
434 int span_arena_stack_n;
435
436 void span_arena_alloc(int sz) {
437 span_arena = malloc(sz * sizeof *span_arena);
438 span_arenasz = sz;
439 span_arena_used = 0;
440 span_arena_stack_n = 0;
441 }
442 void span_arena_free() {
443 free(span_arena);
444 }
445 void span_arena_push() {
446 assert(span_arena_stack_n < SPAN_ARENA_STACK);
447 span_arena_stack[span_arena_stack_n++] = span_arena_used;
448 }
449 void span_arena_pop() {
450 assert(0 < span_arena_stack_n);
451 span_arena_used = span_arena_stack[--span_arena_stack_n];
452 }
453 /* spans_alloc returns a spans which has n equal to the number passed in.
454 Typically the caller will either fill the number requested exactly, or will shorten n if fewer are used.
455 */
456 spans spans_alloc(int n) {
457 assert(span_arena);
458 spans ret = {0};
459 ret.s = span_arena + span_arena_used;
460 ret.n = n;
461 span_arena_used += n;
462 assert(span_arena_used < span_arenasz);
463 return ret;
464 }
465
466 span nullspan() {
467 return (span){0, 0};
468 }
469
470 int bool_neq(int, int);
471
472 int bool_neq(int a, int b) { return ( a || b ) && !( a && b); }
473
474 /*
475 C string routines are a notorious cause of errors.
476 We are adding to our spanio library as needed to replace native C string methods with our own safer approach.
477 We do not use null-terminated strings but instead rely on the explicit end point of our span type.
478 Here we have spanspan(span,span) which is equivalent to strstr or memmem in the C library but for spans rather than C strings or void pointers respectively.
479 We implement spanspan with memmem under the hood so we get the same performance.
480 Like strstr or memmem, the arguments are in haystack, needle order, so remember to call spanspan with the thing you are looking for as the second arg.
481 We return a span which is either NULL (i.e. nullspan()) or starts with the first location of needle and continues to the end of haystack.
482 Examples:
483
484 spanspan "abc" "b" -> "bc"
485 spanspan "abc" "x" -> nullspan
486 */
487
488 span spanspan(span haystack, span needle) {
489 // If needle is empty, return the full haystack as strstr does.
490 if (empty(needle)) return haystack;
491
492 // If the needle is larger than haystack, it cannot be found.
493 if (len(needle) > len(haystack)) return nullspan();
494
495 // Use memmem to find the first occurrence of needle in haystack.
496 void *result = memmem(haystack.buf, len(haystack), needle.buf, len(needle));
497
498 // If not found, return nullspan.
499 if (!result) return nullspan();
500
501 // Return a span starting from the found location to the end of haystack.
502 span found;
503 found.buf = result;
504 found.end = haystack.end;
505 return found;
506 }
507
508 // Checks if a given span is contained in a spans.
509 // Returns 1 if found, 0 otherwise.
510 // Actually a more useful function would return an index or -1, so we don't need another function when we care where the thing is.
511 int is_one_of(span x, spans ys) {
512 for (int i = 0; i < ys.n; ++i) {
513 if (span_eq(x, ys.s[i])) {
514 return 1; // Found
515 }
516 }
517 return 0; // Not found
518 }
519
520 /* END LIBRARY CODE */
In the library we have a ton of hand-written C code that’s designed more to explore library design ideas than to be a mature C library. Part of the problem here is that C is an old language and we have some modern ideas that are easier to support in another language. Choosing C means we have to reimplement the parts of Python or Lisp that would make our problem easy, and the second-order cost is that we have to decide what parts to implement.
After we have the C library code we begin the application code. This is where things get interesting.
521
522 /* StockfishProcess
523
524 This typedef struct contains everything we need and pass around for talking to the stockfish process.
525
526 This includes a pid, an int[2] for the sending pipe and receiving pipe, called to_stockfish and from_stockfish resp. and a u8* cmp_highwater, which we use to know how much data is new in cmp after we pipe some stockfish output into there.
527 */
528
529 typedef struct {
530 pid_t pid; // Process ID of the Stockfish process
531 int to_stockfish[2]; // Pipe for sending data to Stockfish
532 int from_stockfish[2]; // Pipe for receiving data from Stockfish
533 u8* cmp_highwater; // Highwater mark of consumed output from Stockfish in cmp
534 } StockfishProcess;
On line 522-527 we have a block comment that includes a strict enough NL spec that we expect the code to be recreated correctly from it. The PL and NL read almost identically, and we cheated a bit by including “u8* cmp_highwater” in an English sentence even though it’s C syntax, so according to any competent editor, we should spell it out, like “a pointer to byte type” or something like that. The NL could be improved. Presumably at this point, we could feed the file to ChatGPT up to line 528 and it should recreate the code that follows in a form that works.
At this point we see one of the problems with the line-number based approach to chunking a code file, which is that the line numbers change even if we reformat a struct such as this one. To avoid the problem of referring to chunks of a file that are likely to move, we will refer to them by some function of their contents. For the 522-534 section of bpa.c, we can call this chunk “StockfishProcess” for now.
535
536 /*
537 */
538
It’s unclear why there is an empty block comment here, probably to make the function below it easier to navigate to in some early version of the cmpr TUI.
539 void launch_stockfish(StockfishProcess *sp) {
540 // Create pipes
541 if (pipe(sp->to_stockfish) == -1 || pipe(sp->from_stockfish) == -1) {
542 perror("pipe");
543 exit2(EXIT_FAILURE);
544 }
545
546 // Fork the current process
547 sp->pid = fork();
548 if (sp->pid == -1) {
549 perror("fork");
550 exit2(EXIT_FAILURE);
551 } else if (sp->pid == 0) {
552 // Child process: Set up and execute Stockfish
553
554 // Set up pipes for standard input/output
555 dup2(sp->to_stockfish[0], STDIN_FILENO);
556 close(sp->to_stockfish[0]);
557 close(sp->to_stockfish[1]);
558
559 dup2(sp->from_stockfish[1], STDOUT_FILENO);
560 close(sp->from_stockfish[0]);
561 close(sp->from_stockfish[1]);
562
563 // Execute Stockfish
564 execlp("stockfish", "stockfish", (char *)NULL);
565 perror("execlp");
566 exit2(EXIT_FAILURE);
567 } else {
568 // Parent process: Close unused ends of the pipes
569 close(sp->to_stockfish[0]);
570 close(sp->from_stockfish[1]);
571 }
572 }
We’ll call this chunk “launch_stockfish”. This looks like the code was hand-written, or written in response to a prompt recorded elsewhere.
Here we open and communicate with a stockfish process using low-level C functions; it is likely that this was prompted as I would never use EXIT_FAILURE, nor would I likely close the pipes when we are just going to exit anyway.
573
574 #define PRT_STOCKFISH 0
575
576 void send_to_stockfish(StockfishProcess *sp, const char *cmd) {
577 if (PRT_STOCKFISH) prt("sending to stockfish: %s", cmd);
578 write(sp->to_stockfish[1], cmd, strlen(cmd));
579 }
Here we have even more hand-written code related to the above functions. To talk to the stockfish process we have a function that takes a pointer to the process and a command string, which is using here traditional C-style strings. This is different from how we would have done it a few months later, when we were more consistently avoiding C-style strings, but then we are here limited to a 500-line inlined library, so it is interesting to consider how we might structure this part today.
580
581 /*
582 We read output from stockfish and append it to cmp.
583
584 We use a 1k buffer but read again if the buffer was filled by the read() call.
585 We memcpy to cmp.end so that we append, and then extend cmp.end such that len(cmp) will be greater by the amount of data read.
586 */
587
588 void read_from_stockfish(StockfishProcess *sp) {
589 char buffer[1024];
590 ssize_t bytes_read;
591 do {
592 bytes_read = read(sp->from_stockfish[0], buffer, sizeof(buffer) - 1);
593 if (bytes_read > 0) {
594 buffer[bytes_read] = '\0'; // Null-terminate the string
595 // Append the read data to cmp
596 memcpy(cmp.end, buffer, bytes_read);
597 cmp.end += bytes_read; // Update cmp.end to reflect the new data
598 }
599 } while (bytes_read == sizeof(buffer) - 1); // Continue reading if buffer was filled
600 if (PRT_STOCKFISH) {
601 prt("read_from_stockfish:\n");
602 wrs(cmp);terpri();
603 }
604 }
This all makes sense, but because of the storage format of this early project, we can’t tell if this code was written by LLM or edited after that by a human programmer. Probably the convention to record manual edits was still forming at this time. We would write first with line comments in the source code, which we now consider a mistake because regenerating the source code should not require reading through it for comments first. Later we would include both inline code comments and explanation of some kind in prose in a sentence near the end of the NL block comment.
605
606 /*
607 We use cmp_highwater to determine how much of the output in cmp from the stockfish process is new after we send some particular command to stockfish.
608 We manually indicate the highwater mark by calling set_stockfish_highwater and then use get_stockfish_new_output to get a span of the output past this point.
609 */
610
611 void set_stockfish_highwater(StockfishProcess *sp) {
612 sp->cmp_highwater = cmp.end;
613 }
614
615 span get_stockfish_new_output(StockfishProcess *sp) {
616 return (span){sp->cmp_highwater, cmp.end};
617 }
It’s possible that with the prompts we had at some point, this description of how the functions are used was enough to generate the correct code. This is a kind of SICP-like style of short wrappers around other functionality that makes it just slightly more convenient to build on later.
From here, we can summarize the chunks that remain in the file:
MoveEvaluation The move struct The game struct The PGN parser
618
619 /*
620 We use the MoveEvaluation to record each legal move in a position along with the stockfish evaluation of that move.
621 This is what we use to generate the arrows.
622 */
623
624 typedef struct {
625 span lan_move; // The move in Long Algebraic Notation (LAN)
626 int cp_eval; // The centipawn evaluation of the move given by Stockfish
627 } MoveEvaluation;
628
629 /*
630 The move struct is created when we parse our PGN input, and then we add to it as we move through the analysis process.
631 During parsing we generate:
632 - move_number, which we don't actually use for anything but it records what was in the PGN.
633 - san
634 - annotation
635 - comments
636 - move_sequence variations (commented out because not currently used)
637 - the MoveEvaluation *evals
638
639 There's a common pattern of some pointer and an int that go together for a dynamically-allocated array.
640 We only partly support a lot of what's on this struct because we didn't really fully finish the parser, only the parts we needed.
641 */
642
643 typedef struct {
644 int move_number;
645 span san; // Standard Algebraic Notation move text
646 span lan; // Long Algebraic Notation used by stockfish
647 span annotation; // Move annotations like "?!","!!","?", or "!"
648 span comments[10]; // Array of comments
649 int num_comments; // Number of comments
650 //move_sequence variations[5]; // Array of variations
651 int num_variations; // Number of variations
652 MoveEvaluation *evals; // Eval of every legal move from this position
653 int n_evals; // number of evals; equal to number of legal moves at this point
654 } move;
655
656 /*
657 The Game struct includes the metadata on the game, which is called "tags" in PGN, the moves themselves, and any comments on the starting position, which are the only ones that can't be stored on a move.
658
659 We store the tags as a manually-managed array of spans because this predates the `spans` type.
660 We store the moves themselves in a similar way.
661 */
662
663 typedef struct {
664 span *tags; // Array of spans, each representing a tag
665 int tag_count; // Number of tags in the array
666 int max_tags; // Allocated array size
667 move *moves; // Array of moves
668 int move_count; // Number of moves in the array
669 spans startpos_comments; // comments on the starting position (before move 1)
670 } Game;
671
672 /*
673 * Function: parse_pgn
674 * --------------------
675 * Parses a Portable Game Notation (PGN) formatted chess game from the input span.
676 *
677 * The function extracts the sequence of chess moves and relevant metadata from a PGN-encoded chess game.
678 * It handles the basic PGN structure, including tags (metadata enclosed in square brackets)
679 * and the move text (standard algebraic notation), ignoring annotations and variations for simplicity.
680 * The function dynamically allocates memory for an array of spans, each representing a single move,
681 * and returns a Game structure containing this array and the move count.
682 */
683
684 // Global flag for enabling debugging output
685 int debug_mode = 0;
686
687 // Parser functions declarations
688 void parse_pgn(span *input, Game *game);
689 void parse_tag_section(span *input, Game *game);
690 void parse_move_section(span *input, Game *game);
691 span parse_tag(span *input);
692 span parse_result(span *input);
693
694 void parse_pgn(span *input, Game *game) {
695 if (debug_mode) {
696 prt("Entering parse_pgn\n");
697 }
698
699 parse_tag_section(input, game);
700 parse_move_section(input, game);
701
702 if (debug_mode) {
703 prt("Leaving parse_pgn\n");
704 }
705 }
This chunk ll. 672-705 is an overview block for the PGN parser as a whole, which we would give a better name now. This is an example where we want to be able to give a chunk of code a name that’s not already present in the content. We can do that by using a content-based ID currently such as “Function: parse_pgn” and later replacing it with something more descriptive and letting tooling connect the identity of the code across the rename. This is one of the problems with content-based naming is that it’s not always clear when we are re-assigning the identity of something in some mutable system of names.
The PGN parser was built in response to the sample PNG, was written in recursive descent, and was entirely written by ChatGPT. I happen to recall this as the only clear memory of this project, as this was my first large and non-trivial codebase written purely in NL. Getting GPT4 to write a recursive-descent parser in C is an interesting challenge because it requires cross-cutting concerns, not only local ones. This was how I learned about context control in generating correct code with LLMs. The problem with tooling circa chess_bpa is that we could not control the LLM context with any granularity below the file level without some awk or sed scripting. However, we can imagine that if we have a good set of chunks and content-based naming, then we can easily express a solution to the problem is a graph where the nodes are content-named chunks and the edges encode relevance to correct code generation.
An interesting observation about chess_bpa is that it encodes the development history of the project, essentially, in the file contents order, from top to bottom. First we started out with a clear idea of what we wanted to build and a goal for how to do it; then we immediately dove in, imported some skeletal library code, and started to get right into it with PGN parsing, which is also following the execution-time order of the program execution. This is typical of “scripting” rather than “programming” languages, as a scripting language more closely shows the large-scale structure of the execution, while programming languages generally have some other top-level structure such as a class hierarchy. Of course, this is “structured programming” so we have functions, and we will follow one of the C conventions of putting the main function at the end of the file, with all the functions that it calls (which is, transitively, all the functions we need for the build) ahead of it in the file.
So we can describe the build in two ways, from the top of the file down or from the top of the design down. In fact we already took a bottom-up approach by starting with a directory listing, and essentially this is the only way to start when we know literally nothing about a codebase. However, if we know nothing about it, we would also have little reason to look at it. Usually we want to use the program to do something or to figure out if we can use it to do something, or we want to use it to learn how to do what it does. Sometimes changing the program is a way to use it to do something that we currently can’t.
When we want to change the program from the top down, we think about functionality and features, and interactions between the program and humans or other systems.
The reason that we kept everything top-down was because of the development style of feeding the entire program to the LLM, which is much easier if you don’t go back to make changes, because then those must be kept up-to-date. This was where a lot of frustration with the PGN parser and the other parser code came from, because we kept redesigning the foundations (data types, function signatures between the recursive descent functions) while still trying to get the whole program working end-to-end. This is, like whole-program optimization, a kind of whole-level optimization because even the library code is part of the project. Essentially we are trying to see how far down the stack we can go (picking C, etc) before it becomes a problem.
A lot of the design of the original cmpr code graph concept was based around the needs introduced by a complex project like chess_bpa, which is mostly complex because it includes a string library and a PGN parser and a module for communication with Stockfish, whereas in Python all of those would be separate modules provided by the language community. The benefits are that we can include and understand only the code we need, we can get the maximum performance out of the hardware available without going to hand-written assembly, and we never have to deal with library bugs or security issues, only our own. The costs are of course that we have to own the implementation, which should be easier if code is as cheap as the initial LLM hype seemed to suggest. Using C in chess_bpa (and cmpr1) was an attempt to answer the question of whether or not using a low-level language still matters if the LLM can recall the library and syntax details well enough for us to focus on the design.
We hope to eventually demonstrate a better way, which is to have a system that can satisfy a certain need (e.g. communicating with Stockfish) not as a library at all, but as structured context.
706
707 /*
708 skip_whitespace takes a span pointer and advances it past any whitespace characters (as per isspace).
709 */
710
711 void skip_whitespace(span *input) {
712 while (input->buf < input->end && isspace(*input->buf)) {
713 input->buf++;
714 }
715 }
Here we are creating parser utilities inline with the parser. Some of these moved into a real set of parser utilities in later projects.
716 /* parse_tag is called when there is already an open square bracket at the front of the input.
717 It parses up to the closing bracket, skips whitespace, and returns the tag as a span.
718
719 Note: the handling of whitespace needs to be part of the contract.
720 */
721
722 span parse_tag(span *input) {
723 if (debug_mode) prt("enter parse_tag (len %d)\n", len(*input));
724 if (input->buf < input->end && *input->buf == '[') {
725 u8 *start = input->buf++;
726 while (input->buf < input->end && *input->buf != ']') {
727 input->buf++;
728 }
729 if (input->buf < input->end) { // Successfully found closing bracket
730 span tag = {start + 1, input->buf}; // *** manually fixed by adding one to exclude opening bracket ***
731 input->buf++; // Move past the closing bracket
732 if (debug_mode) prt("exit parse_tag success (len %d)\n", len(*input));
733 skip_whitespace(input);
734 return tag;
735 }
736 }
737 if (debug_mode) prt("exit parse_tag no parse (len %d)\n", len(*input));
738 return (span){NULL, NULL}; // Return a null span to indicate failure
739 }
These lines are a very interesting example of what’s going on with this workflow. We have a comment in the PL that says it was manually fixed by adding one. This is because the NL didn’t work, but look at how short it is! This is way less context than we started giving later, because we were trying to put the model to do its best and most impressive expansion of minimalist hints that we provided. This was a kind of prompt golfing that was probably a necessary stage of mastering a specific model, but in the context of building a robust system in a landscape where models are improving, this is mostly a distraction. However, it does give the outlines of what’s been picked up from the training set, which is mostly what learning these current generation models is about.
The note about whitespace being part of the contract is telling. We didn’t formally write a grammar so we didn’t have to decide where in the grammar whitespace is represented, and sometimes we did it inline in the syntax functions and sometimes we did it in two places, or in neither place, and nothing worked.
I recall at this time that I developed some debugging features for the parser as a whole. This was also an experiment in developing software with an LLM.
Often we will not reach for certain approaches because the involve a lot of boilerplate or code churn. Now we have the LLM in between our intention and the compiler, so we can literally give in an existing function, an optional bug report, and ask it to generate that same code but with extensive logging information relevant to the bug at hand. In cmpr1 this was our primary debugging pattern and it proved incredibly effective, and it naturally developed along with the previous-code-with-commentary pattern. In fact it is a natural next step after the code clearly isolates the bug, to say “now fix it” and let the LLM just do the obvious thing. Unfortunately now you have a N-step dependency chain between your initial code and the final debugged result, which implies your NL can be tightened. The story of every bug becomes part of the way the code is.
The exciting thing to me at this time was the possibility of directly applying more formal methods such as correctness proofs of program invariants based on the application of an LLM with some heuristics. It turns out that LLMs are still terrible at any kind of reasoning, so they simply couldn’t be made to do this in 2024. An area for further research.
What turned out to be immediately useful was the the practice of having patterns of editing code in response to some situation entirely by the LLM. The edited code, in the case of performance logging or debugging output can simply be discarded later, because the goal is just to gather some information. So, although this never happened in my experience, even if the LLM introduced a subtle bug while adding the logging information, it wouldn’t matter because we aren’t planning on shipping that code anyway. Of course, this gets into larger SDLC and process issues.
740
741 /*
742 In parse_tag_section, we handle all the metadata at the start of a game.
743 */
744
745 void parse_tag_section(span *input, Game *game) {
746 if (debug_mode) {
747 prt("Entering parse_tag_section\n");
748 }
749
750 // Initialize the tag count and allocate memory for tags array
751 game->tag_count = 0;
752 game->tags = (span *)malloc(sizeof(span));
753 if (game->tags == NULL) {
754 perror("Memory allocation error");
755 exit2(EXIT_FAILURE);
756 }
757
758 while (input->buf < input->end && *input->buf == '[') {
759 span tag = parse_tag(input);
760 if (tag.buf != NULL) {
761 // Reallocate memory for tags array to accommodate the new tag
762 game->tags = (span *)realloc(game->tags, (game->tag_count + 1) * sizeof(span));
763 if (game->tags == NULL) {
764 perror("Memory allocation error");
765 exit2(EXIT_FAILURE);
766 }
767 // Add the new tag to the tags array
768 game->tags[game->tag_count++] = tag;
769 }
770 }
771
772 if (debug_mode) {
773 prt("Leaving parse_tag_section\n");
774 }
775 }
Another thing we are doing here is manually allocating arrays for things wherever needed. This lets us avoid any dependencies and makes it easy for a C programmer to understand the code without having to understand our arena allocator or whatever we would use in a larger project.
776
777 /*
778 * Relevant EBNF Fragment for a Chess Move in PGN:
779 * move ::= move_number move_text { comment }
780 * move_number ::= digit {digit} '.'
781 * move_text ::= letter { letter | digit | symbol }
782 * comment ::= '{' { any_character } '}'
Here we start to introduce EBNF because it’s getting too unmaintainable to have the PGN language we are parsing described only by prose or shown in examples. For things like the aforementioned whitespace issues one easy solution is to have a formal grammar and generate the parser from the grammar using a parser generator such as bison or yacc. Here we have that old compromise which is the worst of both, where the grammar is intended as documentation and probably won’t be kept up to date. However, if the system were to ensure that the PL was continuously generated from the NL, then we would be forced to keep the “documentation” up to date, and it becomes essentially part of the code.
The question for the programmer in charge of the system is this: when can we trust the LLM to reliably interpret the PEG that we give it and write correct code? At what point is the integration of an LLM translation of a PEG dialect into C code more or less trouble than the integration of an established parser-generator tool? Areas for future research. As of 2025 I would try using a library, such as a parser combinator library, with an LLM instructed to use that particular library, to construct a robust system that can turn PEG or some other convenient representation into correct programs in whatever language we need. However, I don’t have a resource to, in fifteen minutes, just get that system off the shelf and have it understand and give me exactly what I need.
This is one vision for a future of software development that’s different from the years of people gluing shit together without understanding how any of it works that many decry as the industry’s current slide into mediocrity. Instead of this we would have systems that are experts in particular very narrow aspects of programming, and we would plug a set of those systems into a super-system, and let that super-system work out the details in the codebase.
For example, we could have a set of established prompts that effectively guide a specific LLM to parse an input PEG using some particular combinator library. This is easy even for an older or cheaper model, if there is a fairly direct translation from the PEG representation to the library calls. However, usually a parser combinator library is, necessarily, tightly integrated with the data types and input-output primitives of the environment. If we are writing C and using some special and unique string library, so all our string representations are different, we won’t be able to use a standard parser-combinator library either. This is a problem in almost every programming language community, especially low-level languages. Either you commit to a batteries-included ecosystem, like Python did, in which case you must be held back by that same library, or you do not commit, and incompatibilities mean that code cannot be re-used effectively across your language community because you need too much scaffolding to build practical software, and the scaffolding that everyone uses is non-standard and different.
We often want to vary these axes independently across projects, with our parsing approach or library and our string or data representation being both dependent on project-specific needs and tradeoffs, so this is a general problem in libraries that solve all kinds of interesting problems. Anything that has unpredictable input-output requirements is going to pose challenges for library or language design. We can solve this problem with complex compiler or pre-processor features or some language-specific type system magic that programmers will have to spend time learning or through some inevitably deferred generics proposal that never materializes, all approaches that various programming communities have tried. However, another approach is to just have one system that knows one library and one that knows how to write the other library. Then you can rewrite the library that needs to vary over the type used by the first library such as our span representation.
We can introduce a little bit of programmer notation, where we will use function calls in s-exps.
(define ((compile build) artifact)) where artifact is something that we ship, such as the bpa binary, and the build is all the inputs, such as a tree full of files and a build script. We are treating (define) here as pseudo-code to represent a function that takes a code tree from a clean state to a state with a new compiled binary in some output directory. For example, our actual “compile” implementation in this project is literally a bare gcc command. Then we can (define ((generate parser) build_component)) where the input is a grammar of some kind and the output is a C file, which we have to integrate into our build, or a file or function or module or object which we have to integrate into our build. Now we have introduced a necessary step before the compilation step, and we have split our build into components so we have to know something about these components. Our process to change the program is now something like edit the grammar, regenerate the parser, and recompile. So we are returning (compile (concat (generate parser) other-components)) as our artifact.
If we use an LLM to generate code from a PEG, how is this different from a library that deterministically does that? In the library case we have to defend against bugs in the library but in the LLM-generated code we have to consider bugs in newly-generated and untested code, unless our system takes care of this for us.
783 *
784 * We have a convenience function parse_move_number which returns an int.
785 * This function also consumes the dots (one or three) that follow the number.
786 * We do not need to know how many there were because we already know who has the move by previous moves.
Here we are strongly hinting to the LLM to throw away information about the dots, otherwise it (GPT4) is more likely to declare a variable etc to hold this state, and then maybe do something weird like not use the variable and get a compiler warning, which our aggressive build rules will treat as an error, etc, … costing us a whole dev-test cycle for something we can avoid.
A lot of the early prompting experiments went like this, with me probing the model to see how subtly I can convey what it needs to know before it gets it wrong. Later I had a reference system, so instead we can just reference everything explicitly and state clearly what to write in the reference docs–you know, just like writing software for people.
787 *
788 * We will call another function, parse_san, to parse the actual SAN move.
789 * The move may be followed by some kind of annotation like "?!" which we will also parse.
790 *
791 * We can use w_char to print an individual character.
792 *
793 * Sample Moves:
794 * 1. d4 { [%eval 0.15] [%clk 0:10:00] }
795 * 1... d5 { [%eval 0.25] [%clk 0:10:00] }
796 * 4. Bg5?! { [%eval -0.55] } { Inaccuracy. Nc3 was best. } { [%clk 0:09:46] }
797 *
798 * We handle variations by recursing into a parse_move_sequence function.
799 * They look like this:
800 *
801 * (4. Nc3 e6 5. Bg5 Be7 6. e3 O-O 7. Bd3 dxc4 8. Bxc4 c5)
802 * We consume the open paren and then recurse, and then consume the close paren.
803 *
804 * Note that we also must parse O-O and O-O-O.
805 */
806
807 void print_move(move m) {
808 prt("Move Number: %d\n", m.move_number);
809 prt("SAN: %.*s\n", m.san.end - m.san.buf, m.san.buf);
810 prt("Annotation: %.*s\n", m.annotation.end - m.annotation.buf, m.annotation.buf);
811
812 // Print comments
813 prt("Comments:\n");
814 for (int i = 0; i < m.num_comments; i++) {
815 prt(" %.*s\n", m.comments[i].end - m.comments[i].buf, m.comments[i].buf);
816 }
817
818 /*
819 // Print variations
820 prt("Variations:\n");
821 for (int i = 0; i < m.num_variations; i++) {
822 prt(" Variation %d:\n", i + 1);
823 print_move_sequence(m.variations[i]); // Assuming print_move_sequence is defined to handle move_sequence type
824 }
825 */
826 }
I remember that the print_move function was written and maintained by hand, and we didn’t think anything about having it in this block with the parse functions, I guess, possibly because it’s just the order in which it was written. I imagine we had some stub in here written by the LLM, and then we came in later and wrote the actual printing code by hand. This is of course the best way to do it, because everything in that code is going to change in a thousand tiny ways before we are happy with it.
827
828 // Forward declaration of the parse_move_sequence function for recursion
829 void parse_move_sequence(span *input);
830 span parse_san(span *input);
831 int parse_move_number(span*);
This is just some C shit that we drop here without comment. We need the forward declaration because if you don’t have a header file or otherwise declare your functions in advance then you can’t call them before they are defined. However, you can declare them and then call them and then define them, which we do–almost never, because we define functions before calling them in this file, which is why it ends with main().
832
833 /*
834 In parse_move we handle a single half-move in the PGN, including comments and variations which follow it.
835 */
836
837 move parse_move(span *input) {
838 move m = {0};
839 //m.num_comments = 0;
840 //m.num_variations = 0;
841
842 // Parse move number and consume dots
843 if (isdigit(*input->buf)) m.move_number = parse_move_number(input);
844
845 skip_whitespace(input);
846
847 // Parse SAN move
848 m.san = parse_san(input);
849
850 // Parse any annotations like "?!","!!","?", or "!"
851 span annotation = {input->buf, input->buf};
852 if (*input->buf == '!' || *input->buf == '?') {
853 annotation.buf = input->buf++;
854 while (*input->buf == '!' || *input->buf == '?') {
855 input->buf++;
856 }
857 annotation.end = input->buf;
858 }
859 m.annotation = annotation;
860
861 skip_whitespace(input);
862
863 // Parse comments
864 while (*input->buf == '{') {
865 if (m.num_comments >= 10) {
866 prt("Array bounds exceeded for comments.\n");
867 exit2(1); // Exit to fix the code
868 }
869
870 input->buf++; // Skip '{'
871 span comment = {input->buf, NULL};
872 while (*input->buf != '}') {
873 if (input->buf >= input->end) {
874 prt("Comment not properly closed.\n");
875 exit2(1); // Exit to fix the code
876 }
877 input->buf++;
878 }
879 comment.end = input->buf;
880 m.comments[m.num_comments++] = comment;
881 input->buf++; // Skip '}'
882 skip_whitespace(input); // Skip any whitespace after the comment
883 }
884
885 // Parse variations
886 while (*input->buf == '(') {
887 //if (m.num_variations >= 5) {
888 // prt("Array bounds exceeded for variations.\n");
889 //exit2(1); // Exit to fix the code
890 //}
891
892 input->buf++; // Consume the '('
893 parse_move_sequence(input); // Parse the variation
894 if (*input->buf != ')') {
895 prt("Variation not properly closed with ')'.\n");
896 exit2(1); // Exit to fix the code
897 }
898 input->buf++; // Consume the ')'
899 m.num_variations++;
900 skip_whitespace(input); // Skip any whitespace after the variation
901 }
902
903 if (debug_mode) {
904 prt("end of parse_move\n");
905 dbgd(len(*input));
906 }
907 return m;
908 }
We can see from the commented-out lines of code that this is heavily edited and manually maintained probably for a while. What I did in this project was to let ChatGPT write all the code, but then understand and debug it myself. Not unreasonable for a small hobby project, but it ended up being a deep dive into how GPT makes mistakes, at least in writing parsers in C. The commented-out code here exits the loop if the counter is too high, which is great way to debug a parser that was hand-written and has none of the basic capabilities that any parser generator worth using would give you for free, like high-quality error messages. So what we may find here are remnants of me debugging a vibe-coded (before that was a term) recursive descent parser for a format that I was also seeing for the first time. Basically, once I generated the code, if it looked good to me, but didn’t run, then I treated it as a traditional debugging problem, and solved it myself. Obviously asking GPT4 to debug code that it wrote is pointless; it is terrible at reasoning and good at pattern-matching, so if it knows the solution it would have solved the problem, and if it does not, trying again with the previous code and a generic instruction will not help. However, there is a lot more to say about getting later models to debug code.
909
910 /*
911 In parse_move_number we handle the digits themselves, and either one or three dots following the digits.
912 */
913
914 int parse_move_number(span *input) {
915 if (debug_mode) {
916 prt("entering parse_move_number\n");
917 dbgd(len(*input));
918 }
919
920 int move_number = 0;
921
922 // Convert the sequence of digits to an integer
923 while (input->buf < input->end && isdigit(*input->buf)) {
924 move_number = move_number * 10 + (*input->buf - '0');
925 input->buf++;
926 }
We are even manually parsing integers here, this is refreshingly readable code even though it’s not the fastest way of doing it. We are criticizing the code here as if we are maintaining it, but another option is to not care about it and not read it; in that case we would have to trust the behavior, including performance.
927
928 // Expecting at least one dot after the move number
929 if (input->buf < input->end && *input->buf == '.') {
930 input->buf++; // Skip the first dot
931
932 // Handle ellipsis for black's moves (three dots)
933 while (input->buf < input->end && *input->buf == '.') {
934 input->buf++;
935 }
936 } else {
937 prt("Expected dot after move number.\n");
938 flush();
939 exit2(1); // Exit if the format is incorrect
940 }
941
942 if (debug_mode) {
943 prt("end of parse_move_number: returning %d\n", move_number);
944 dbgd(len(*input));
945 }
946 return move_number;
947 }
So reading through this is a great way to remember the details of the PGN format. However, I’d much rather just see a 16-line PEG and never have to think about it again.
948
949 /*
950 We have is_san_char to indicate which chars can be part of a SAN move, and then parsing a SAN move is as simple as reading these chars into a span until we hit something that's not one of these.
951 */
952
953 int is_san_char(char c) {
954 return isalpha(c) || // Letter for piece or column
955 isdigit(c) || // Digit for row
956 c == 'x' || // Capture indicator
957 c == '=' || // Promotion indicator
958 c == 'O' || // Castling
959 c == '+' || // Check indicator
960 c == '#' || // Checkmate indicator
961 c == '-' // Part of castling notation
962 ;
963 }
What’s both awesome and terrifying about all this code is that we’re vertically integrating everything from our own understanding of the syntax through to the low-level implementation, unrolling what could be a regex in a grammar spec into a manual C expression where we control things like the order of function calls, on lines 954-962. Of course, this can be optimized, but so can anything in a parser generator, which is one reason why you shouldn’t optimize a parser generator until you have a good benchmark.
Note that the model will start to pick up our formatting conventions if we have good examples. This code was generated with reference to all the code above it, so if we have things nicely aligned as they are here, then later blocks are more likely to follow pleasing visual alignment, although GPT4 tends to miss the actual precise alignment because of tokenization and other issues but we can usually fix whatever it is trying to do. A lot of this early code was manually fixed up for formatting, because this is before I stopped wanting to look at or read the generated code.
964
965 /*
966 In parse_san we loop and accumulate chars into a span as long as they can be part of a SAN move, and then we also consume whitespace.
967 */
968
969 span parse_san(span *input) {
970 if (debug_mode) prt("enter parse_san (len %d)\n", len(*input));
971 span san_move;
972 san_move.buf = input->buf; // Start of the SAN move
973
974 while (input->buf < input->end && (
975 isalpha(*input->buf) || // Letter for piece or column
976 isdigit(*input->buf) || // Digit for row
977 *input->buf == 'x' || // Capture indicator
978 *input->buf == '=' || // Promotion indicator
979 *input->buf == 'O' || // Castling
980 *input->buf == '+' || // Check indicator
981 *input->buf == '#' || // Checkmate indicator
982 *input->buf == '-' // Part of castling notation
983 )) {
984 input->buf++; // Advance through the SAN move
985 }
986
987 san_move.end = input->buf; // End of the SAN move
988
989 // Skip any trailing spaces after the SAN move
990 skip_whitespace(input);
991
992 if (debug_mode) {
993 prt("end of parse_san\n");
994 w_char(*input->buf);terpri();
995 dbgd(len(*input));
996 }
997
998 return san_move;
999 }
So just writing and reviewing this code was a lot of work, when all of this could be a single line in a PEG grammar. However, the goal of writing this code was also to understand the PGN notation and to experiment with this kind of LLM assistance. Recursive-descent parsers are infinitely flexible and they are a good way to explore a corpus interactively, if you can iterate fast enough. Another problem with this parser is that we wrote some large chunks of it at once, overly optimistic over some earlier success vibe coding smaller parsers that GPT4 could more reliably get right. There were actually a lot of little reasons why this larger parser was much harder to get right, which determined some process thinking after chess_bpa.
1000
1001 /*
1002 In parse_move_sequence (possibly mis-named?) we consume the moves inside a variation, which is enclosed in parens in PGN format.
1003 We look for the closing paren to know when the "move sequence" (really a variation) is ending, but we don't consume it (the caller will do that).
1004 */
1005
1006 void parse_move_sequence(span *input) {
1007 skip_whitespace(input);
1008 while (input->buf < input->end && *input->buf != ')') {
1009 move m = parse_move(input); // Recursively parse each move in the sequence
1010 if (debug_mode) print_move(m);
1011 skip_whitespace(input);
1012 }
1013 if (debug_mode) {
1014 prt("after parse_move_sequence\n");
1015 dbgd(len(*input));
1016 }
1017 }
1018
1019 /*
1020 In parse_result we parse one of the four PGN result strings, and return a span.
1021 A null span indicates to the caller that nothing was parsed.
1022 The reason for this is that we parse a game by first trying to parse a result and only then, if there is no result at that position, then trying to parse a move.
1023 In debug mode we also print the game result that was parsed.
1024 */
1025
1026 span parse_result(span *input) {
1027 static const char *results[] = {"1-0", "0-1", "1/2-1/2", "*"};
1028 span result = {NULL, NULL};
1029 for (int i = 0; i < 4; ++i) {
1030 size_t len = strlen(results[i]);
1031 if ((input->end - input->buf) >= len && strncmp((char *)input->buf, results[i], len) == 0) {
1032 result.buf = input->buf;
1033 result.end = input->buf + len;
1034 if (debug_mode) {
1035 prt("Parsed game result: %.*s\n", (int)len, input->buf);
1036 }
1037 input->buf += len; // Move the input buffer forward
1038 break;
1039 }
1040 }
1041 return result;
1042 }
1043
1044 /*
1045 In parse_move_section we parse out the rest of the PGN file after the metadata in the tags at the top.
1046
1047 The only thing we are currently using from this is the move sequence on the main line.
1048
1049 There can be comments in a PGN before the first move, these comments apply to the starting position, so first we parse those and store them on startpos_comments on the game if there are any.
1050 We call a helper function parse_startpos_comments(span*, Game*) to handle all of this.
1051
1052 In a loop we first try to parse a result, which is one of a fixed number of short strings.
1053 If parse_result doesn't parse anything we then try to parse a move by calling parse_move, which handles everything from the move number on.
1054
1055 Everything in the move section will be handled by either parse_move or parse_result, i.e. comments and variations are both handled inside of parse_move.
1056
1057 In debug mode we indicate entering and leaving the function, as with all our parsing functions.
1058 */
1059
1060 void parse_startpos_comments(span*, Game*);
1061
1062 void parse_move_section(span *input, Game *game) {
1063 if (debug_mode) {
1064 prt("Entering parse_move_section\n");
1065 }
1066
1067 parse_startpos_comments(input, game);
1068
1069 // Dynamically allocate an initial buffer for moves, may need resizing
1070 int capacity = 10; // Initial capacity for moves
1071 game->moves = malloc(capacity * sizeof(move));
1072 game->move_count = 0;
1073
1074 while (input->buf < input->end) {
1075 span resultSpan = parse_result(input);
1076 if (resultSpan.buf == NULL) { // No game result found, proceed to parse move
1077 if (game->move_count == capacity) {
1078 // Resize the array if we've reached capacity
1079 capacity *= 2;
1080 game->moves = realloc(game->moves, capacity * sizeof(move));
1081 }
1082 move mv = parse_move(input); // Parse the next move
1083 game->moves[game->move_count++] = mv; // Add the move to the Game
1084 } else {
1085 // If we've parsed a result, it's the end of the move section.
1086 break;
1087 }
1088 }
1089
1090 if (debug_mode) {
1091 prt("Leaving parse_move_section with %d moves parsed.\n", game->move_count);
1092 }
1093 }
1094
1095 /*
1096 In print_game we print the SAN moves only, as a PGN parser debugging function.
1097 We use wrs and terpri to simply print each SAN move span on the Game.
1098 */
1099
1100 void print_game(Game game) {
1101 for (size_t i = 0; i < game.move_count; ++i) {
1102 wrs(game.moves[i].san); // Print the move
1103 terpri(); // Move to the next line
1104 }
1105 }
1106 /*
1107 There can be comments in a PGN before the first move, these comments apply to the starting position, so first we parse those and store them on startpos_comments on the game if there are any.
1108 We handle this with parse_startpos_comments().
On line 1106 we start a comment block without a blank line before, which was not done previously in the file. This probably marks where we switched to use block-oriented editing in the cmpr TUI, which we were starting to prototype at the same time.
1109
1110 In debug mode, we prt when we enter and exit from the function, the length of the input span before and after, and the number of comments we parsed (and added to the Game).
1111
1112 We need to alloc the right number of spans, so first we call parse_comment in a loop just to count the comments.
1113 We use empty() to determine if parse_comment consumed a comment or not.
1114 Then we call spans_alloc with this number, and that is the value that we store on the Game.
1115
1116 We forward declare parse_comment, which takes the input span and returns either an empty span if there is no comment at the beginning of the input, or returns
1117 a span containing the comment text, with the braces removed.
1118 Note this means we cannot roundtrip empty comments, but that's fine.
1119 */
Later we had nl2pl comments, and this is the sort of thing we would comment out, because it’s meta-commentary about the limitations of the implementation, which generally doesn’t help the model write effective code.
1120
1121 // Forward declaration of parse_comment
1122 span parse_comment(span *input);
1123
1124 void parse_startpos_comments(span *input, Game *game) {
1125 if (debug_mode) {
1126 prt("Entering parse_startpos_comments\n");
1127 dbgd(len(*input));
1128 }
1129
1130 int comment_count = 0;
1131
1132 // Count comments before the first move
1133 span test_input = *input; // Copy input to avoid modifying the original
1134 while (1) {
1135 span comment = parse_comment(&test_input);
1136 if (empty(comment)) break; // No more comments to process
1137 comment_count++;
1138 }
1139
1140 // Allocate space for comments
1141 game->startpos_comments = spans_alloc(comment_count);
1142 game->startpos_comments.n = comment_count;
1143
1144 // Actually parse and store the comments
1145 for (int i = 0; i < comment_count; i++) {
1146 span comment = parse_comment(input); // This time modify the original input
1147 game->startpos_comments.s[i] = comment;
1148 }
1149
1150 if (debug_mode) {
1151 prt("Exiting parse_startpos_comments\n");
1152 dbgd(len(*input));
1153 dbgd(comment_count);
1154 }
1155 }
1156 /*
1157 In parse_comment, we skip any initial whitespace, then consume a opening curly brace if there is one.
1158 We read up to the next closing curly brace, and set the span between (and exclusive of) the curly braces as our return value.
1159 If there is no opening curly brace then there is no comment to parse and we indicate this by returning a null span.
1160 If we do not find a closing brace for a comment, then we print a message and abort as usual (prt, flush, exit).
1161 If we have parsed a comment, we also call skip_whitespace again to consume any trailing whitespace after the comment.
1162
1163 Note that comments cannot nest as per the PGN spec and also they cannot contain curly braces at all (e.g. lichess just strips them if you try).
1164 */
1165
1166 span parse_comment(span *input) {
1167 skip_whitespace(input); // Skip initial whitespace
1168
1169 if (input->buf < input->end && *input->buf == '{') {
1170 input->buf++; // Skip opening curly brace
1171
1172 span comment_start = *input; // Start of comment text
1173
1174 // Search for closing brace
1175 while (input->buf < input->end && *input->buf != '}') {
1176 input->buf++;
1177 }
1178
1179 if (input->buf < input->end) { // Found closing brace
1180 span comment = {comment_start.buf, input->buf};
1181 input->buf++; // Skip closing brace
1182 skip_whitespace(input); // Skip trailing whitespace
1183 return comment; // Return span containing the comment
1184 } else {
1185 // Did not find a closing brace, which is an error
1186 prt("Error: Comment not closed with a '}'.\n");
1187 flush();
1188 exit(EXIT_FAILURE);
1189 }
1190 }
1191
1192 // No opening curly brace, so no comment to parse
1193 return (span){NULL, NULL};
1194 }
1195
1196 /*
1197 SAN to LAN:
1198
1199 We are given a Game object which contains the SAN moves from the PGN for all the moves in the game.
1200
1201 Our task here is to convert each SAN move (like Nc3) into a LAN move (like b1c3), which we need to communicate with stockfish.
1202
1203 In order to do that, we need to figure out the starting square that the piece (or pawn) was on.
1204 The destination square is already given in the SAN.
1205
1206 The SAN does not contain the starting square (and the LAN does) but rather it contains the piece (or file for a pawn) and further contains disambiguation info, which is either a file, a rank, or both (in order of preference) as necessary if there would otherwise be more than one legal move matching the description in the given position.
1207 Therefore we need to consider both the current position and potentially the set of legal moves in that position in order to find the correct starting square.
1208
1209 Here's our approach:
1210
1211 1. Get position from stockfish as FEN string.
1212 2. Get candidate starting squares from the position, according to the piece type (or pawn file) and any disambiguation in the SAN.
1213 - for example, if the SAN move is R8c6 then we only find rooks on the 8th rank (however, we will consider any rook on the 8th rank, regardless of the file, we do not also restrict to the c file, because we don't want this function to have to know how the rook moves; that is handled by the next step).
1214 3. Get legal moves as LAN from stockfish if needed for disambiguation. (If there is more than one piece of the given type (or pawn on the given file) on the board (and matching the SAN disambiguation if any) then only one of them can be a legal move, because otherwise the SAN would have contained further disambiguation.)
1215
1216 */
1217 /* void poll_stockfish(span, int, StockfishProcess*);
1218
1219 We read from stockfish in a loop, waiting a few ms each time, until the output contains the string provided.
1220 If that never happens, we will loop forever.
1221 To prevent this, we limit the maximum wait time to the second argument, which is in milliseconds.
1222 If the limit is reached, we print a warning and exit the process.
1223 */
1224
1225 void poll_stockfish(span target, int max_wait_ms, StockfishProcess *sp) {
1226 int total_wait = 0;
1227 int wait_interval_ms = 10; // Interval to wait between polls in milliseconds
1228 span new_output = (span){sp->cmp_highwater, cmp.end};
1229 while (!contains(new_output, target)) {
1230 usleep(wait_interval_ms * 1000); // Sleep for wait_interval_ms milliseconds
1231 read_from_stockfish(sp); // Read the current output from Stockfish
1232 new_output.end = cmp.end;
1233 total_wait += wait_interval_ms;
1234 if (total_wait > max_wait_ms) {
1235 prt("Warning: Max wait time of %d ms exceeded while waiting for \"%.*s\".\n", max_wait_ms, len(target), target.buf);
1236 flush();
1237 exit(EXIT_FAILURE);
1238 }
1239 }
1240 }
1241
1242 spans get_legal_lan_moves(StockfishProcess *sp);
1243
1244 /*
1245 We get the legal moves by sending "go perft 1" to stockfish.
1246 We poll the stockfish response until it contains the string "Nodes searched".
1247
1248 example stockfish session demonstrating getting the 20 legal moves in the starting position:
1249
1250 input to stockfish:
1251 position startpos
1252 go perft 1
1253
1254 output from stockfish:
1255 a2a3: 1
1256 b2b3: 1
1257 c2c3: 1
1258 d2d3: 1
1259 e2e3: 1
1260 f2f3: 1
1261 g2g3: 1
1262 h2h3: 1
1263 a2a4: 1
1264 b2b4: 1
1265 c2c4: 1
1266 d2d4: 1
1267 e2e4: 1
1268 f2f4: 1
1269 g2g4: 1
1270 h2h4: 1
1271 b1a3: 1
1272 b1c3: 1
1273 g1f3: 1
1274 g1h3: 1
1275
1276 Nodes searched: 20
1277
1278 In this function we assume that stockfish has already been given the current position, so we just send the go command.
This is pretty wild cowboy coding, where we have a stockfish process in a state, which we just have to know is going to be the state we need when we are in this function. This is the kind of program where you have to have the whole thing in your head to make sense of it, and it has some internal state. Probably not a lot, because probably the stockfish process state is pretty easy to understand, but it’s something the programmer has to be aware of. The LLM as of 2025 probably can’t be trusted to get it right, although this can easily be corrected by some expository context.
1279 We determine the number of positions from the output, use spans_alloc() to get a spans of that size, and then put each LAN move as a span into the spans, which we return.
1280 To parse the output of stockfish, we create a new span which is a copy of cmp, but which we will mutate as we parse it.
1281 *** manually fixed this to use the new get_stockfish_new_output() and set_stockfish_highwater() ***
1282 In a loop, to parse the LAN moves out of the output (see example above):
1283 We use find_char to find the first colon, and take_n() to consume up to that colon.
1284 Then we can advance to the newline and consume that.
1285 Empty lines in the output will be skipped.
1286 Also, the Nodes searched line, which also contains a colon, should be skipped, not added as another "move".
1287 So once we have pulled out a span up to the colon, we check if it contains the string "Nodes searched" and if so we do not add it as a move.
1288 */
1289
1290 spans get_legal_lan_moves(StockfishProcess *sp) {
1291 set_stockfish_highwater(sp);
1292
1293 send_to_stockfish(sp, "go perft 1\n");
1294 flush(); // Ensure command is sent to Stockfish
1295
1296 span target = S("Nodes searched");
1297 poll_stockfish(target, 5000, sp); // Wait for Stockfish to output "Nodes searched", up to 5 seconds
1298
1299 // Prepare to parse the output
1300 //span work = cmp; // Make a working copy of cmp to parse
1301 span work = get_stockfish_new_output(sp);
1302 int moves_count = 0;
1303 spans moves = spans_alloc(20); // Allocate space for up to 20 moves initially, assuming standard opening move count
1304
1305 while (!empty(work)) {
1306 int newline_pos = find_char(work, '\n');
1307 if (newline_pos == -1) break; // No more lines to process
1308
1309 span line = first_n(work, newline_pos);
1310 int colon_pos = find_char(line, ':');
1311 if (colon_pos != -1) {
1312 span move_span = first_n(line, colon_pos);
1313 if (!span_eq(move_span, target)) { // Ignore "Nodes searched" line
1314 if (moves_count >= moves.n) {
1315 // Expand the moves array if necessary
1316 spans new_moves = spans_alloc(moves.n * 2); // Double the capacity
1317 memcpy(new_moves.s, moves.s, moves.n * sizeof(span)); // Copy existing moves
1318 moves = new_moves;
1319 }
1320 moves.s[moves_count++] = move_span;
1321 }
1322 }
1323 work.buf += newline_pos + 1; // Advance past the newline
1324 }
1325
1326 moves.n = moves_count; // Update the count of moves
1327 return moves;
1328 }
1329
1330 void populate_lan_moves(Game*, StockfishProcess*);
1331
1332 void send_position(StockfishProcess *sp, Game *game, int upto_move);
1333 span correlate_san_with_lan(span san_move, spans legal_lan_moves);
1334
1335 /*
1336 The struct SanDetails stores information on the SAN move, which our PGN parser leaves intact as a span.
1337 Before we use it to find the corresponding LAN moves to send to stockfish, we need to parse it a bit further.
1338 We also indicate here whether the move is for white (with is_white_move), even though that's not part of the SAN format itself.
1339 Otherwise everything we include is from the SAN itself.
1340 - is_white_move
1341 - piece_moved
1342 - is_capture
1343 - disambiguation
1344 - destination_square
1345 - promotion_piece
1346 - check_indicator
1347 */
1348
1349 typedef struct {
1350 int is_white_move; // which player the move is for *** manually added ***
1351 char piece_moved; // The type of piece moved (R, N, B, Q, K, a-h for pawn)
1352 int is_capture; // If the move is a capture *** manually added ***
1353 char disambiguation[3]; // Disambiguation information, can be a file ('a'-'h'), a rank ('1'-'8'), or both, e.g., "e2"
1354 char destination_square[3]; // The destination square of the move, e.g., "e4"
1355 char promotion_piece; // The piece a pawn is promoted to, if applicable ('Q', 'R', 'B', 'N'). 0 if not a promotion.
1356 char check_indicator; // '+' for check, '#' for checkmate, 0 if neither.
1357 } SanDetails;
1358
1359 /*
1360 A nice pretty-printer for SanDetails.
1361 *** manually fixed a bit ***
1362 */
1363
1364 void pretty_print_san_details(SanDetails details) {
1365 prt("Move Details:\n");
1366 prt("Player: %s\n", details.is_white_move ? "White" : "Black");
1367 prt("Piece Moved: %c\n", details.piece_moved);
1368 prt("Capture: %s\n", details.is_capture ? "Yes" : "No");
1369 prt("Disambiguation: %s\n", details.disambiguation[0] ? details.disambiguation : "None");
1370 prt("Destination Square: %s\n", details.destination_square);
1371 prt("Promotion: %c\n", details.promotion_piece ? details.promotion_piece : '0');
1372 prt("Check/Checkmate: %c\n", details.check_indicator ? details.check_indicator : '0');
1373 }
1374
1375 /*
1376 In populate_lan_moves, we are given a Game which has SAN moves from the PGN, but does not have the LAN moves that we need for stockfish.
1377
1378 To get the LAN moves, for each move in the game, we
1379 - tell stockfish the current position by the send_position() helper function.
1380 - parse the SAN to get:
1381 - the piece (or pawn) type that was moved, one of R N B Q K or a-h for a pawn move, and
1382 - the disambiguation information if any, which can be a rank, a file, or neither or both, and
1383 - the destination square, and
1384 - the piece promoted to if the move was a pawn promotion, and
1385 - the check or checkmate indication, which our san parsing function also finds but which we aren't using here.
1386 - we pass i % 2 == 0 into parse_san_details since it needs to know the move for handling castles
1387 - get the position as FEN from stockfish
1388 - call a helper function which uses the parsed SAN and the FEN info to return the square containing the piece that moved
1389 - this function can optionally call stockfish to get the legal moves, which is also uses if needed
1390 - then we simply concatenate the algebraic start square and end square, along with the lowercased promotion piece, if there is one, to get the LAN move which is always 4 or 5 chars, 5 being in the case of pawn promotions.
1391 - add this LAN move to the move.lan
1392
1393 Invariant: we always have a LAN move for moves prior to the current move, and we don't have LAN moves for any later ones.
1394 Once we have reached the end of the game then all the LAN moves are populated and we are done.
1395 */
1396
1397 SanDetails parse_san_details(span, int);
1398 void get_fen_from_stockfish(StockfishProcess*, char*, size_t);
1399 void find_start_square(char *fen, SanDetails san_details, char *start_square, StockfishProcess *sp);
1400 void assign_lan_move(move*, char*);
1401
1402 void populate_lan_moves(Game *game, StockfishProcess *sp) {
1403 char fen[256]; // Buffer to hold FEN string
1404
1405 for (int i = 0; i < game->move_count; ++i) {
1406 // Set position in Stockfish up to the current move
1407 send_position(sp, game, i);
1408
1409 // Parse the SAN details for the current move
1410 SanDetails san_details = parse_san_details(game->moves[i].san, i % 2 == 0);
1411
1412 // Get the current position as FEN from Stockfish
1413 get_fen_from_stockfish(sp, fen, sizeof(fen));
1414
1415 // Find the starting square based on FEN and parsed SAN details
1416 char start_square[3]; // Buffer to hold the starting square
1417 find_start_square(fen, san_details, start_square, sp);
1418
1419 // Construct the LAN move by concatenating the start square, the destination square,
1420 // and optionally the lowercased promotion piece
1421 char lan_move[6]; // Buffer to hold the LAN move
1422 if (san_details.promotion_piece) {
1423 snprintf(lan_move, sizeof(lan_move), "%s%s%c", start_square, san_details.destination_square, tolower(san_details.promotion_piece));
1424 } else {
1425 snprintf(lan_move, sizeof(lan_move), "%s%s", start_square, san_details.destination_square);
1426 }
1427
1428 // Assign the constructed LAN move to the current move in the game
1429 assign_lan_move(&game->moves[i], lan_move);
1430 }
1431 }
1432
1433 /*
1434 In send_position, we construct a position string which contains "position startpos moves" followed by all the moves up to the argument passed in.
1435 We get the LAN format that stockfish uses and separate them by spaces.
1436 Then we send this string to stockfish, putting it in the position at that point in the game.
1437 */
1438
1439 void send_position(StockfishProcess *sp, Game *game, int upto_move) {
1440 // Start with setting the position to the start position
1441 char position_str[4096] = "position startpos moves";
1442 int len = strlen(position_str);
1443
1444 // Append each LAN move up to the specified move number, separated by spaces
1445 for (int i = 0; i < upto_move && i < game->move_count; ++i) {
1446 // Assuming LAN moves are stored in game->moves[i].lan
1447 int move_len = game->moves[i].lan.end - game->moves[i].lan.buf;
1448
1449 // Check if the move fits into the remaining buffer space, accounting for the space and null terminator
1450 if (len + move_len + 2 < sizeof(position_str)) {
1451 position_str[len++] = ' '; // Add space before the move
1452 strncpy(position_str + len, (char*)game->moves[i].lan.buf, move_len);
1453 len += move_len;
1454 } else {
1455 // Handle error: position string buffer overflow
1456 prt("Error: position string buffer overflow.\n"); flush();
1457 exit(EXIT_FAILURE);
1458 }
1459 }
1460
1461 // Null-terminate the position string
1462 position_str[len] = '\0';
1463
1464 //prt("sending position string: %s\n", position_str);
1465 // Send the constructed position string to Stockfish
1466 send_to_stockfish(sp, position_str);
1467 send_to_stockfish(sp, "\n"); // Ensure command is properly terminated
1468 }
1469
1470 /*
1471 parse_san_details gets a SAN move and an indication of whether it is white or black to move.
1472 The details are parsed into the SanDetails return type.
1473 Since the check indication is the last thing, we check for it first, and update the length accordingly, then do the same for the promotion if any.
1474 We record the piece name or pawn file into piece_moved.
1475 If the "piece" is "O" then it is castles, which is why we need to know who has the move, to determine the destination square; these are recorded as king moves and the destination square is set to g1 or c1 or g8 or c8 as appropriate.
1476 Then the last two remaining chars that we haven't already handled will always be the destination square.
1477 The char before that will be an "x" if the move was a capture.
1478 Finally any remaining chars not already handled will be disambiguation chars, of which there may be 0 to 2.
1479 */
1480
1481 SanDetails parse_san_details(span san, int is_white_move) {
1482 SanDetails details = {0};
1483 details.is_white_move = is_white_move;
1484
1485 int length = san.end - san.buf;
1486
1487 // Handle check or checkmate indicator
1488 if (san.buf[length - 1] == '+' || san.buf[length - 1] == '#') {
1489 details.check_indicator = san.buf[length - 1];
1490 length--; // Adjust length to exclude check/checkmate indicator
1491 }
1492
1493 // Handle promotion
1494 if (length > 2 && san.buf[length - 2] == '=') {
1495 details.promotion_piece = san.buf[length - 1];
1496 length -= 2; // Adjust length to exclude promotion information
1497 }
1498
1499 // Determine if the move is a capture
1500 // *** manually fixed *** from:
1501
1502 /*
1503 if (san.buf[length - 3] == 'x') {
1504 details.is_capture = 1;
1505 length -= 1; // Adjust length to exclude 'x'
1506 }
1507
1508 to: */
1509 if (san.buf[length - 3] == 'x') {
1510 details.is_capture = 1;
1511 }
1512 /* *** end manual fixup *** */
1513
1514 // Set destination square
1515 details.destination_square[0] = san.buf[length - 2];
1516 details.destination_square[1] = san.buf[length - 1];
1517 details.destination_square[2] = '\0'; // Null-terminate
1518
1519 // Castling
1520 if (san.buf[0] == 'O') {
1521 details.piece_moved = 'K'; // Castling is a king move
1522 // Determine the destination square based on castling side and who's moving
1523 strcpy(details.destination_square, is_white_move ? (length == 3 ? "g1" : "c1") : (length == 3 ? "g8" : "c8"));
1524 } else {
1525 // For non-castling moves, the first character might indicate the piece moved or be part of a pawn move
1526 if (isalpha(san.buf[0]) && san.buf[0] != 'x') {
1527 details.piece_moved = san.buf[0];
1528 } else {
1529 // *** manually added ***
1530 assert(0); // dead code
1531 // Pawn moves are indicated by file ('a'-'h')
1532 details.piece_moved = san.buf[length - 4]; // Assuming the move format includes the starting file for pawns
1533 }
1534
1535 // Handle disambiguation, if any
1536 int disambiguationLength = length - 3 - details.is_capture; // Calculate length excluding destination square and capture indicator
1537 if (disambiguationLength > 0 && details.piece_moved != 'K') { // Exclude castling
1538 strncpy(details.disambiguation, san.buf + 1, disambiguationLength);
1539 details.disambiguation[disambiguationLength] = '\0'; // Null-terminate
1540 }
1541 }
1542
1543 return details;
1544 }
1545
1546 /*
1547 The "d" command gets a representation of the current board position from stockfish in several forms like this:
1548
1549 +---+---+---+---+---+---+---+---+
1550 | r | n | b | q | k | b | n | r | 8
1551 +---+---+---+---+---+---+---+---+
1552 | p | p | p | p | p | p | p | p | 7
1553 +---+---+---+---+---+---+---+---+
1554 | | | | | | | | | 6
1555 +---+---+---+---+---+---+---+---+
1556 | | | | | | | | | 5
1557 +---+---+---+---+---+---+---+---+
1558 | | | | | | | | | 4
1559 +---+---+---+---+---+---+---+---+
1560 | | | | | | | | | 3
1561 +---+---+---+---+---+---+---+---+
1562 | P | P | P | P | P | P | P | P | 2
1563 +---+---+---+---+---+---+---+---+
1564 | R | N | B | Q | K | B | N | R | 1
1565 +---+---+---+---+---+---+---+---+
1566 a b c d e f g h
1567
1568 Fen: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
1569 Key: 8F8F01D4562F59FB
1570 Checkers:
1571
1572 After sending "d\n", get_fen_from_stockfish polls until "Fen" appears, and then handles the output.
1573
1574 We only care about the FEN, so we parse it line by line until we find a line starting with "Fen: ",
1575 strip this prefix, and return the FEN string in the buffer provided by the caller.
1576 *** manually fixed to start from sp->cmp_highwater instead of cmp.buf *** (can use the method for this)
1577 */
1578
1579 void get_fen_from_stockfish(StockfishProcess *sp, char *fen, size_t fen_size) {
1580 set_stockfish_highwater(sp);
1581
1582 // Send "d" to Stockfish to display the current board position and various info
1583 send_to_stockfish(sp, "d\n");
1584
1585 // Poll Stockfish output until "Fen" is found
1586 span target = S("Fen");
1587 poll_stockfish(target, 5000, sp); // Wait up to 5000 ms for "Fen" to appear
1588
1589 // At this point, cmp contains the output including "Fen"
1590 // Parse cmp line by line to find the FEN string
1591 //char *line_start = (char *)cmp.buf;
1592 char *line_start = (char *)sp->cmp_highwater;
1593 char *fen_start;
1594 while ((line_start = strstr(line_start, "\n")) != NULL) {
1595 // Move past the newline character
1596 line_start++;
1597
1598 // Check if the current line starts with "Fen: "
1599 if (strncmp(line_start, "Fen: ", 5) == 0) {
1600 // Found the FEN string
1601 fen_start = line_start + 5; // Skip past "Fen: "
1602 char *fen_end = strchr(fen_start, '\n');
1603 if (fen_end != NULL) {
1604 size_t fen_length = fen_end - fen_start;
1605 if (fen_length < fen_size) {
1606 // Copy the FEN string to the buffer provided by the caller
1607 strncpy(fen, fen_start, fen_length);
1608 fen[fen_length] = '\0'; // Null-terminate the FEN string
1609 return; // Successfully found and copied the FEN string
1610 }
1611 }
1612 }
1613 }
1614
1615 // If we reach here, either we didn't find "Fen: " or there was a buffer size issue
1616 fprintf(stderr, "Failed to find or copy the FEN string from Stockfish's output.\n");
1617 exit(EXIT_FAILURE); // Exit indicating failure to get FEN
1618 }
1619
1620 /*
1621 In find_start_square we get a FEN string, a parsed SanDetails from a SAN move, and a destination buffer which will always be at least 3 chars long.
1622 We also have the StockfishProcess, which we may need to get a list of legal moves if needed to find a unique solution.
1623
1624 First we find all the squares in the position that contain a piece of the type that moved.
1625 In the case of a pawn move, this already includes the file that the pawn is on, which along with the destination square uniquely determines the starting square of the move.
1626
1627 In the case of a piece, we narrow the list of potential starting squares by the SAN disambiguation if any.
1628 However, there may still be more than one piece of the given type in our list.
1629 If this is the case, we then get the legal moves from stockfish; these will be in the LAN format.
1630 If there was no disambiguation in the SAN move, it is because only one of the pieces of the given type can reach the destination square.
1631 In this case we determine the starting square by looking for a legal move which has a starting square from our list, and there will only be one such.
1632 *** editing because the above was wrong ***
1633 In this case we determine the starting square by looking for a legal move which has a starting square from our list and the destination square matching the SAN.
1634
1635 Examples:
1636 Q8xd7. We find all the queens (of the right color) from the FEN, then filter out any that are not on the 8th rank. This gives a list of candidate starting squares. If there is more than one, that means there are two queens on the 8th rank; we then ask stockfish for the legal moves; only one of the queens will be able to reach the destination square, so we are done.
1637 Be3. In most cases there may be two bishops, and we will find the squares they are not and get the legal moves to see which one can reach the destination.
1638 e4. In this case it is already unambiguous since only one pawn can ever reach any given square without capture.
1639 fxe4. The capture case is also unambiguous since the starting file is given for pawn captures.
1640
1641 First we'll call a helper function that finds the squares that are compatible with the SAN move, i.e. the piece or pawn type and the disambiguation if any.
1642 This will return a spans with the candidate algebraic square names.
1643 If this list has only one element then we are done, and we put it in start_square and return.
1644 Otherwise, the list has more than one element.
1645 We get the legal moves from stockfish, which also gives us a spans.
1646 Then we loop over the candidate squares and, inside that, over the legal moves, and the first match that we find will be the result.
1647 If we haven't found any match, some assumption in our code is incorrect so we report the error and simply crash.
1648 We can also create a helper function to determine whether a candidate square is the start square of a LAN move.
1649 */
1650
1651 // Declaration for the helper functions assumed by the find_start_square implementation
1652 spans find_candidate_squares(char *fen, SanDetails san_details);
1653 int is_start_square_of_lan_move(span candidate_square, span lan_move);
1654 int is_destination_square_match(span lan_move, SanDetails san_details);
1655
1656 void find_start_square(char *fen, SanDetails san_details, char *start_square, StockfishProcess *sp) {
1657 //prt("find_start_square: %s\n", fen);
1658 //pretty_print_san_details(san_details);
1659 // Find candidate starting squares based on the piece and any disambiguation
1660 spans candidate_squares = find_candidate_squares(fen, san_details);
1661 //dbgd(candidate_squares.n);
1662
1663 if (candidate_squares.n == 1) {
1664 // If only one candidate, it's our starting square
1665 strncpy(start_square, (char*)candidate_squares.s[0].buf, 2);
1666 start_square[2] = '\0'; // Ensure null-termination
1667 } else {
1668 // If more than one candidate, get legal moves from Stockfish to resolve ambiguity
1669 spans legal_moves = get_legal_lan_moves(sp);
1670
1671 // Iterate over candidate squares and legal moves to find a match
1672 int found = 0;
1673 for (int i = 0; i < candidate_squares.n; ++i) {
1674 //prt("candidate square: ");wrs(candidate_squares.s[i]);terpri();
1675 for (int j = 0; j < legal_moves.n; ++j) {
1676 //prt("legal move: ");wrs(legal_moves.s[j]);terpri();
1677 if (is_start_square_of_lan_move(candidate_squares.s[i], legal_moves.s[j])
1678 // *** next line added manually ***
1679 && is_destination_square_match(legal_moves.s[j], san_details)) {
1680 //prt("Found!\n");
1681 // Found the starting square
1682 strncpy(start_square, (char*)candidate_squares.s[i].buf, 2);
1683 start_square[2] = '\0'; // Ensure null-termination
1684 found = 1;
1685 break; // Stop searching once found
1686 }
1687 }
1688 if (found) break; // Stop searching once found
1689 }
1690
1691 if (!found) {
1692 // If no match found, something went wrong
1693 prt("Error: Failed to find starting square for move.\n");flush();
1694 exit(EXIT_FAILURE); // Crash the program
1695 }
1696 }
1697 }
1698
1699 /*
1700 To find candidate squares we iterate over the FEN string, and we find each square containing the piece and color given by the SAN details.
1701 We have a static string of length 128 that contains all the square names in the same layout as FEN (i.e. a8 through h1 in memory) and we construct spans (each of len() 2) that point into that.
1702 We have a helper function that takes a single char from the FEN, the file, and our SanDetails and returns whether or not the square is a potential match.
1703 (The file is needed only for pawns.)
1704 If there is disambiguation info in the SAN we only return the squares that are consistent with it, otherwise any square that has the right kind of piece; we declare a helper function first that handles this disambiguation info.
1705 For pawn moves, the result should always already be deterministic; we have a file in piece_moved and we remember that "p" or "P" is in the FEN.
1706 FEN uses digits to indicate runs of blank squares, we handle these by incrementing the file we are on.
1707 We use the rank and file to index into the square names correctly.
1708 We use spans_alloc for the return variable.
1709 Note that spans_alloc returns a spans with .n set to the size provided, so we use a separate variable result_count to track the number of results and set .n at the end.
1710 We can allocate space for 10 spans as that is a maximum number of possible pieces of the same type that can be on the board in standard chess.
1711 */
1712
1713 int square_matches_piece(char fen_char, int file, char piece, int is_white_move);
1714 int matches_disambiguation(const char *square, const char *disambiguation);
1715
1716 spans find_candidate_squares(char *fen, SanDetails san_details) {
1717 static const char square_names[] =
1718 "a8b8c8d8e8f8g8h8"
1719 "a7b7c7d7e7f7g7h7"
1720 "a6b6c6d6e6f6g6h6"
1721 "a5b5c5d5e5f5g5h5"
1722 "a4b4c4d4e4f4g4h4"
1723 "a3b3c3d3e3f3g3h3"
1724 "a2b2c2d2e2f2g2h2"
1725 "a1b1c1d1e1f1g1h1";
1726
1727 spans result = spans_alloc(10); // Allocate space for up to 10 candidate squares
1728 int result_count = 0;
1729
1730 int square_index = 0; // Index to iterate over square_names
1731 int rank = 8, file = 0; // Start from the 8th rank and 'a' file
1732
1733 for (int fen_index = 0; fen[fen_index] != ' '; ++fen_index) {
1734 char fen_char = fen[fen_index];
1735 if (fen_char == '/') {
1736 rank--; // Move to the next rank
1737 file = 0; // Reset file to 'a'
1738 continue;
1739 }
1740
1741 if (isdigit(fen_char)) {
1742 file += fen_char - '0'; // Skip empty squares
1743 } else {
1744 if (square_matches_piece(fen_char, file, san_details.piece_moved, san_details.is_white_move)) {
1745 char current_square[3] = {file + 'a', rank + '0', '\0'};
1746
1747 if (matches_disambiguation(current_square, san_details.disambiguation)) {
1748 // Construct a span for the current square and add to result
1749 int name_index = ((8 - rank) * 16) + (file * 2); // Calculate index in square_names
1750 result.s[result_count] = (span){(u8*)square_names + name_index, (u8*)square_names + name_index + 2};
1751 result_count++;
1752 }
1753 }
1754 file++; // Move to the next file
1755 }
1756 }
1757
1758 result.n = result_count; // Update the count of result spans
1759 return result;
1760 }
1761
1762 /*
1763 Helper function to print spans for debugging
1764 */
1765
1766 void print_spans(spans s) {
1767 printf("Candidate Squares:\n");
1768 for (int i = 0; i < s.n; ++i) {
1769 printf("%.*s\n", s.s[i].end - s.s[i].buf, s.s[i].buf);
1770 }
1771 }
1772
1773 /*
1774 This is part of finding the candidate starting squares for a SAN move, given that we also have a FEN of the current position.
1775
1776 In square_matches_piece we get a FEN char, one of KQBNRP upper or lower, a file, and a SAN piece char, one of 'a'-'h' lowercase, or KQBNR, which are upper for both w and b in SAN moves.
1777 We get a file which tells where the FEN char was in the FEN, and an indication of who has the move.
1778 We return true if the FEN char matches the piece type, color, and in the case of a pawn, the file indicated by the SAN piece char.
1779 */
1780
1781 int square_matches_piece(char fen_char, int file, char piece, int is_white_move) {
1782 // Check color and piece type
1783 int is_fen_char_white = isupper(fen_char);
1784 char fen_piece_type = toupper(fen_char);
1785 //prt("square_matches_piece: %c %d %c %d\n", fen_char, file, piece, is_white_move);
1786
1787 // Determine if the SAN piece represents a pawn move (indicated by a file 'a'-'h')
1788 int is_pawn_move = (piece >= 'a' && piece <= 'h');
1789
1790 // Match color
1791 if (bool_neq(is_white_move, is_fen_char_white)) {
1792 return 0; // Color mismatch
1793 }
1794
1795 // For pawns: Check if the file matches the SAN piece (indicating the pawn's file)
1796 if (is_pawn_move) {
1797 int pawn_file = piece - 'a'; // Convert 'a'-'h' to 0-7 for file comparison
1798 return (fen_piece_type == 'P' && file == pawn_file);
1799 }
1800
1801 // For non-pawn pieces: Check if the piece types match
1802 // Convert SAN piece to corresponding FEN character
1803 char san_piece_to_fen;
1804 switch (piece) {
1805 case 'K': san_piece_to_fen = 'K'; break;
1806 case 'Q': san_piece_to_fen = 'Q'; break;
1807 case 'B': san_piece_to_fen = 'B'; break;
1808 case 'N': san_piece_to_fen = 'N'; break;
1809 case 'R': san_piece_to_fen = 'R'; break;
1810 default: return 0; // Invalid piece character
1811 }
1812
1813 //dbgd(fen_piece_type == san_piece_to_fen);flush();
1814 return (fen_piece_type == san_piece_to_fen);
1815 }
1816 /*
1817 Helper function to check if the current square matches the disambiguation criteria.
1818 The SAN disambiguation is either a rank "a"-"h" or a file "1"-"8" or both like "d4", or nothing.
1819 It's provided as three chars, so the first can be either a rank or file, and the second will always be a rank if anything.
1820 We return false if any disambiguation is present and doesn't match and true otherwise.
1821 */
1822
1823 int matches_disambiguation(const char *square, const char *disambiguation) {
1824 // If disambiguation is empty, there's nothing to match against, so it's considered a match
1825 if (disambiguation[0] == '\0') {
1826 return 1;
1827 }
1828
1829 // Disambiguation can be file, rank, or both
1830 // Check if the first character of disambiguation matches the file (square[0]) or rank (square[1])
1831 if (isalpha(disambiguation[0])) { // File disambiguation
1832 if (square[0] != disambiguation[0]) {
1833 return 0; // File doesn't match
1834 }
1835 }
1836 if (isdigit(disambiguation[0])) { // Rank disambiguation, or file was a digit, which is an error in this context
1837 if (square[1] != disambiguation[0]) {
1838 return 0; // Rank doesn't match
1839 }
1840 }
1841
1842 // If there's a second character and it's a digit, it must be rank disambiguation
1843 if (disambiguation[1] != '\0' && isdigit(disambiguation[1])) {
1844 if (square[1] != disambiguation[1]) {
1845 return 0; // Rank doesn't match
1846 }
1847 }
1848
1849 // Passed all checks, so it's a match
1850 return 1;
1851 }
1852
1853 /*
1854 Helper function to check if a LAN move matches a square passed in as a (len() = 2) span.
1855 Example:
1856 "a3" "a3a4" -> 1
1857 "a3" "d2d4" -> 0
1858 The spans passed in will always be of the correct length so we do not need to test for that.
1859 */
1860
1861 int is_start_square_of_lan_move(span candidate_square, span lan_move) {
1862 //prt("is_start_square_of_lan_move\n");
1863 //wrs(candidate_square);terpri();
1864 //wrs(lan_move);terpri();
1865 // Since spans are guaranteed to be the correct length, we directly compare the first two characters
1866 if (candidate_square.buf[0] == lan_move.buf[0] && candidate_square.buf[1] == lan_move.buf[1]) {
1867 return 1; // The start square of the LAN move matches the candidate square
1868 } else {
1869 return 0; // No match
1870 }
1871 }
1872
1873 int is_destination_square_match(span lan_move, SanDetails san_details);
1874
1875 /*
1876 Helper function to determine if a LAN move matches the destination square of a SAN move.
1877 The destination square of a SAN move is always 2 chars and so we can just do a direct comparison of the two chars with the second two chars of the LAN move.
1878 We do not need to check the length of the LAN move passed in, as we know it will be correct.
1879 We simply compare the 3rd and 4th characters of the lan move with the two of the SAN details destination square.
1880 */
1881
1882 int is_destination_square_match(span lan_move, SanDetails san_details) {
1883 // Direct comparison of the destination square with the last two characters of the LAN move
1884 // Note: LAN move format is "e2e4" or "e7e8q" for promotion, so characters at positions 2 and 3 (0-based index) are the destination square
1885 return lan_move.buf[2] == san_details.destination_square[0] && lan_move.buf[3] == san_details.destination_square[1];
1886 }
1887
1888 /*
1889 We are given a char* for a LAN move and must assign it to move* m as a span.
1890 The char* data is on the stack, so we must copy the char* data somewhere before it goes out of scope.
1891 We use the cmp space for this.
1892 We can use prt2cmp() to redirect all output to the cmp space, then use prt to write the data, with newlines before and after it.
1893 Then we call prt2std() so that we don't change the output mode.
1894 We point the lan span on the move to the data that we have just added, without the newlines.
1895 We know how long the data is so we can just subtract from cmp.end.
1896 LAN moves are always 4 or 5 chars long, so we assert that our span is always the correct length before returning.
1897 Don't forget the len() function exists.
1898 */
1899
1900 void assign_lan_move(move *m, char *lan_move) {
1901 // Redirect output to the cmp space
1902 prt2cmp();
1903
1904 // Write the LAN move with newlines before and after to ensure it's isolated
1905 prt("\n%s\n", lan_move);
1906
1907 // Redirect output back to standard output
1908 prt2std();
1909
1910 // Calculate the start of the LAN move in the cmp space (skipping the initial newline)
1911 u8 *lan_start = cmp.end - strlen(lan_move) - 1; // Subtract the length of the LAN move and the newline after it
1912
1913 // Point the lan span in the move to the data just added, excluding newlines
1914 m->lan.buf = lan_start;
1915 m->lan.end = lan_start + strlen(lan_move);
1916
1917 // Assert the length is either 4 or 5 chars
1918 assert(len(m->lan) == 4 || len(m->lan) == 5);
1919 }
1920
1921 void do_analysis(Game*, StockfishProcess*);
1922
1923 /*
1924 To actually do the analysis, we send each position in the game to stockfish.
1925 In each position we then send "go movetime 1000" to analyze for 1 second.
1926 We read all the output from stockfish, which contains info lines that have the values we are interested in.
1927 Here is an example:
1928
1929 info depth 13 seldepth 22 multipv 1 score cp -26 nodes 681621 nps 680260 hashfull 302 tbhits 0 time 1002 pv g8f6 b1c3 e7e6 c1g5 f8e7 e2e3 e8g8 g5h4 f6e4 h4e7 d8e7 d1c2 e4f6 f1d3 d5c4 d3c4
1930 info depth 13 seldepth 13 multipv 2 score cp -38 nodes 681621 nps 680260 hashfull 302 tbhits 0 time 1002 pv e7e6 e2e3 g8f6 b2b3 b8d7 c1b2 b7b6 f1d3 c8b7 e1g1 f8d6 b1d2 e8g8
1931 info depth 13 seldepth 17 multipv 3 score cp -38 nodes 681621 nps 680260 hashfull 302 tbhits 0 time 1002 pv a7a6 c4c5 g8f6 b1c3 g7g6 c1f4 f6h5 f4e5 f8g7 e5g7 h5g7 h2h3 b8d7 e2e3 e8g8 f1d3
1932 [... more lines ...]
1933
1934 The multipv is used to order the lines from best to worst, but since we are going to process all of them we don't care about this.
1935 The only information we need is the first move after "pv", which is the move that we will use for our arrow, and the number after "cp" which is the eval in centipawns.
1936 The lines may be repeated for the same moves, but we can handle this by parsing all of them and updating the cp eval for each one so we will always have the latest result that stockfish gives.
1937
1938 So in this function we just iterate over all the moves in the game, and call a helper function that does the analysis.
1939 As we have a place on the move struct to store the evals, here we just call send_position to update the stockfish process with the current position.
1940 We then call analyze_move to get the evals, and we pass a pointer to the move into this function so that it can store them.
1941 */
1942
1943 void analyze_move(StockfishProcess *sp, move *m);
1944 void analyze_move_2(StockfishProcess *sp, move *m);
1945
1946 void do_analysis(Game *game, StockfishProcess *sp) {
1947 for (int i = 0; i < game->move_count; ++i) {
1948 // Set the position in Stockfish up to the current move
1949 send_position(sp, game, i);
1950
1951 // Analyze the current move and store the evaluations
1952 analyze_move_2(sp, &game->moves[i]);
1953 }
1954 }
1955
1956 /*
1957 In analyze_move, stockfish already has the position, so we just need to send the "go movetime 1000" command to let it evaluate all the legal moves for 1 second.
1958 Before we call send_to_stockfish, we first must call set_stockfish_highwater so that we can tell later where the output from this particular command started.
1959 After the send_to_stockfish call we then sleep for the same number of milliseconds that we put in the movetime.
1960 Then we send "stop" to stockfish, just in case it is still producing output.
1961 We wait for 250ms after sending stop, then we call read_from_stockfish to get its output.
1962 Then we pass the move into a further helper function which will parse the lines in cmp.
1963 We call new_output to get the output after the previous highwater mark which was generated by our command, which we also pass into the helper function.
1964 It is not necessary to send flush() to send commands to stockfish; in fact this has nothing to do with stockfish but actually flushes our output space to stdout, so stop calling flush after send_to_stockfish.
1965
1966 We had a race condition here, which we first tried to fix by waiting, and then later fixed more completely by checking if the moves that stockfish evaluates are legal moves for the player who has the move in the current position.
1967 However, this does not fully solve the problem, as for example, a queen that captures a queen will still have most of the same legal moves (in LAN) on the next half-move.
1968 Perhaps there's other info in the info line that we can use to disambiguate further.
1969 */
1970
1971 void parse_stockfish_output(span output, move *m);
1972 void parse_stockfish_output_2(span output, move *m, spans legal_moves);
1973
1974 void analyze_move(StockfishProcess *sp, move *m) {
1975 // Set highwater mark for Stockfish output to identify new output generated by this command
1976 set_stockfish_highwater(sp);
1977
1978 // Send command to Stockfish to evaluate the position for 1 second
1979 send_to_stockfish(sp, "go movetime 1000\n");
1980
1981 // Sleep for 1 second to allow Stockfish to evaluate
1982 usleep(1000 * 1000); // Sleep for 1000 milliseconds
1983
1984 // Send "stop" to Stockfish to halt evaluation, in case it's still running
1985 send_to_stockfish(sp, "stop\n");
1986
1987 usleep(250 * 1000);
1988
1989 // Read output from Stockfish
1990 read_from_stockfish(sp);
1991
1992 // Get the new output generated by our "go movetime 1000" command
1993 span output = get_stockfish_new_output(sp);
1994
1995 // Parse the Stockfish output to extract move evaluations and update the move structure
1996 parse_stockfish_output(output, m);
1997 }
1998
1999 // Global variable for Stockfish analysis time in milliseconds
2000 int analysis_time_ms = 1000; // Default value
2001
2002 void analyze_move_2(StockfishProcess *sp, move *m) {
2003 // Set highwater mark for Stockfish output to identify new output generated by this command
2004 set_stockfish_highwater(sp);
2005
2006 // Prepare the command string with the global variable for analysis time
2007 char command[256];
2008 snprintf(command, sizeof(command), "go movetime %d\n", analysis_time_ms);
2009
2010 // Send command to Stockfish to evaluate the position for the specified analysis time
2011 send_to_stockfish(sp, command);
2012
2013 // Sleep for the specified analysis time to allow Stockfish to evaluate
2014 usleep(analysis_time_ms * 1000); // Convert milliseconds to microseconds
2015
2016 // Send "stop" to Stockfish to halt evaluation, in case it's still running
2017 send_to_stockfish(sp, "stop\n");
2018
2019 // Increase the wait time after sending stop to ensure all output is captured
2020 //usleep(250 * 1000); // Wait for an additional 250 milliseconds
2021
2022 // Read output from Stockfish
2023 read_from_stockfish(sp);
2024
2025 // Get the new output generated by our command
2026 span output = get_stockfish_new_output(sp);
2027
2028 // Parse the Stockfish output to extract move evaluations and update the move structure
2029 spans legal_moves = get_legal_lan_moves(sp); /* *** manual fixup *** */
2030 parse_stockfish_output_2(output, m, legal_moves);
2031 }
2032
2033 /*
2034 In parse_stockfish_output, we are given a span containing the "info" lines like those shown above, e.g.:
2035
2036 info depth 13 seldepth 22 multipv 1 score cp -26 nodes 681621 nps 680260 hashfull 302 tbhits 0 time 1002 pv g8f6 b1c3 e7e6 c1g5 f8e7 e2e3 e8g8 g5h4 f6e4 h4e7 d8e7 d1c2 e4f6 f1d3 d5c4 d3c4
2037
2038 Here is another sample line that indicates a forced checkmate line, in this case, mate in one for the other player if c2a4 is played by this player.
2039
2040 info depth 232 seldepth 3 multipv 3 score mate -1 nodes 754894 nps 5353858 tbhits 0 time 141 pv c2a4 a8a4
2041 info depth 1 seldepth 2 multipv 2 score mate 6 nodes 1258 nps 629000 tbhits 0 time 2 pv c7b7 b1a1
2042
2043 From the info line we need only the cp eval (e.g. -26 above) or mate and the first LAN move after the pv, which identifies the first move in the line.
2044 Since we are evaluating every legal move, there will be one of these for each legal move in the position.
2045 We extract the LAN move into a span, after checking whether it is four or five chars long.
2046 We optimistically assume that we can locate the LAN move by searching for " pv ".
2047
2048 Sometimes we get a cp eval and sometimes we will get a forced checkmate eval like "score mate -7".
2049 To handle both cases we can look for " score ", then check whether it is followed by "cp " or "mate ", and then handle the number after that.
2050 We do not distinguish in our later analysis between mate and extreme centipawn values so we will just replace any mate with a large eval like +10000 or -10000.
2051 This way we can continue to treat the cp eval as an integer downstream of this parsing function.
2052
2053 The input may contain other lines that do not start with "info", which we must ignore.
2054 We can make one pass over all the lines.
2055
2056 First we parse the line and get the first move, in the example above this is g8f6.
2057 We then look in the evals already stored on the move, and see if this is already present.
2058 If it is, we update the cp eval, since the later lines from stockfish override the earlier ones.
2059 If it is not, then we must add it, and also increment the number of move evals.
2060
2061 Before we start, we can allocate the move eval structs, probably 128 is a safe number as we're unlikely to find more legal moves than that in any game.
2062 However, if we do, we should detect it, report it (using prt followed by flush) and then crash.
2063
2064 We declare helper functions for any part of the process that seems potentially involved.
2065 We will have a helper function update_or_add_eval taking a move*, a span for the lan move, and the cp_eval as an int.
2066 */
2067
2068 void update_or_add_eval(move *m, span lan_move, int cp_eval);
2069
2070 void parse_stockfish_output(span output, move *m) {
2071 char *line = output.buf;
2072 char *end = output.end;
2073
2074 // Prepare for parsing
2075 m->evals = (MoveEvaluation *)malloc(128 * sizeof(MoveEvaluation));
2076 if (!m->evals) {
2077 printf("Memory allocation failed\n");
2078 exit(EXIT_FAILURE); // Fail loudly on allocation failure
2079 }
2080 m->n_evals = 0;
2081
2082 while (line < end) {
2083 char *line_end = strchr(line, '\n');
2084 if (!line_end) break; /* *** manual fixup *** */
2085 //if (!line_end) line_end = end;
2086
2087 if (strncmp(line, "info", 4) == 0) {
2088 char *score_ptr = strstr(line, " score ");
2089 int cp_eval = 0;
2090 if (score_ptr && score_ptr < line_end) {
2091 /* *** score_type is a manual fixup *** */
2092 char *score_type = strstr(score_ptr, "cp ");
2093 if (score_type && score_type < line_end) {
2094 cp_eval = atoi(score_ptr + strlen(" score cp "));
2095 } else if ((score_type = strstr(score_ptr, "mate ")) && score_type < line_end) {
2096 // Treat checkmate as large positive or negative value
2097 int mate_in = atoi(score_ptr + strlen(" score mate "));
2098 cp_eval = mate_in > 0 ? 10000 : -10000; // Simplified representation for mate
2099 } else {
2100 prt("can't parse score: %.*s\n", line_end - line, line);
2101 continue;
2102 //flush();exit(1);
2103 }
2104 }
2105
2106 // Locate the first LAN move after "pv"
2107 char *pv_ptr = strstr(line, " pv ");
2108 if (pv_ptr) {
2109 pv_ptr += 4; // Skip " pv "
2110 int move_length = (*(pv_ptr + 4) == ' ' || *(pv_ptr + 4) == '\n') ? 4 : 5;
2111 span lan_move = {pv_ptr, pv_ptr + move_length};
2112
2113 // Update or add eval for the move
2114 update_or_add_eval(m, lan_move, cp_eval);
2115 }
2116 }
2117
2118 // Move to the start of the next line
2119 line = line_end + 1;
2120 }
2121
2122 // Check for excessive legal moves
2123 if (m->n_evals > 128) {
2124 printf("Error: More than 128 legal moves found, exceeding allocation.\n");
2125 exit(EXIT_FAILURE); // Fail loudly if unexpectedly high number of moves
2126 }
2127 }
2128
2129 /*
2130 Above was our first implementation of parse_stockfish_output. It eventually worked but required some manual fixups.
2131
2132 Let us rewrite this code using span methods instead of char* C-style strings.
2133 In particular, this will solve the issue that we had in the original code of incorrectly searching (via strstr) past the end of the line that we had found, since strstr obviously looks until the next null byte, which does not exist in our input.
2134
2135 Here we rewrite parse_stockfish_output function as parse_stockfish_output_2 using spans throughout.
2136 Additionally, we had a race condition in the above code where we might be getting stockfish output from the previous position, with moves for the other player.
2137 To handle this, we first call get_legal_lan_moves to get all the legal moves from stockfish in the current position.
2138 Then after find_pv_move when we have the lan move that we're about to add to the evals, before actually adding it we call another helper function to tell us if this span is one of the spans in the legal_moves.
2139 If it isn't, we simply skip it, and this solves the issue with incorrectly adding arrows from the previous half-move.
2140 */
2141
2142 // Declaration of additional helper functions that might be needed
2143 int parse_cp_eval(span line);
2144 span find_pv_move(span line);
2145
2146 // Assume declaration of get_legal_lan_moves and is_legal_move helper functions
2147 int is_legal_move(span lan_move, spans legal_moves); // Checks if a LAN move is in the legal_moves list
2148
2149 void parse_stockfish_output_2(span output, move *m, spans legal_moves) {
2150
2151 // Prepare for parsing
2152 m->evals = (MoveEvaluation *)malloc(128 * sizeof(MoveEvaluation));
2153 if (!m->evals) {
2154 prt("Memory allocation failed\n");
2155 flush();
2156 exit(EXIT_FAILURE); // Fail loudly on allocation failure
2157 }
2158 m->n_evals = 0;
2159
2160 while (!empty(output)) {
2161 span line = next_line(&output); // Extract the next line as a span
2162
2163 if (consume_prefix(&line, S("info"))) {
2164 int cp_eval = parse_cp_eval(line); // Parse the cp or mate score
2165 span lan_move = find_pv_move(line); // Find the first LAN move after "pv"
2166
2167 // Check if the LAN move is legal before updating or adding eval
2168 if (!empty(lan_move) && is_legal_move(lan_move, legal_moves)) {
2169 // Update or add eval for the move
2170 update_or_add_eval(m, lan_move, cp_eval);
2171 }
2172 }
2173 }
2174
2175 // Check for excessive legal moves
2176 if (m->n_evals > 128) {
2177 prt("Error: More than 128 legal moves found, exceeding allocation.\n");
2178 flush();
2179 exit(EXIT_FAILURE); // Fail loudly if unexpectedly high number of moves
2180 }
2181 }
2182
2183 // Wrapper to check if a LAN move is legal.
2184 // Uses is_one_of to search through the legal_moves provided.
2185 int is_legal_move(span lan_move, spans legal_moves) {
2186 return is_one_of(lan_move, legal_moves);
2187 }
2188
2189 /*
2190 In parse_cp_eval we get a line and parse either a "score cp <int>" or "score mate <int>" out of it.
2191 We convert the forced mate to either + or - 10000 so that they can be treated as cp evals downstream of this function.
2192 We use spanspan to find the location of " score " and indicate failure if it isn't found.
2193 We return the maximally negative int in case the line doesn't contain " score " or can't be parsed for some other reason.
2194 We get back from spanspan the string starting at that point " score " so we skip past that and then we look after this point for "cp " or "move " with consume_prefix() and then handle the int that follows.
2195
2196 Example input lines:
2197
2198 info depth 13 seldepth 22 multipv 1 score cp -26 nodes 681621 nps 680260 hashfull 302 tbhits 0 time 1002 pv g8f6 b1c3 e7e6 c1g5 f8e7 e2e3 e8g8 g5h4 f6e4 h4e7 d8e7 d1c2 e4f6 f1d3 d5c4 d3c4
2199 info depth 232 seldepth 3 multipv 3 score mate -1 nodes 754894 nps 5353858 tbhits 0 time 141 pv c2a4 a8a4
2200 info depth 1 seldepth 2 multipv 2 score mate 6 nodes 1258 nps 629000 tbhits 0 time 2 pv c7b7 b1a1
2201
2202 Correct outputs:
2203
2204 -26
2205 -10000
2206 10000
2207 */
2208
2209 int parse_cp_eval(span line) {
2210 span score_span = S(" score ");
2211 span cp_span = S("cp ");
2212 span mate_span = S("mate ");
2213 int fail_val = INT_MIN; // Return maximally negative int on failure
2214
2215 // Find " score " in the line
2216 span score_section = spanspan(line, score_span);
2217 if (empty(score_section)) return fail_val; // " score " not found
2218
2219 // Move past " score "
2220 consume_prefix(&score_section, score_span);
2221
2222 // Check for "cp " or "mate "
2223 if (consume_prefix(&score_section, cp_span)) {
2224 // Parse cp value
2225 return atoi((char *)score_section.buf);
2226 } else if (consume_prefix(&score_section, mate_span)) {
2227 // Parse mate value and convert to large cp value
2228 int mate_in = atoi((char *)score_section.buf);
2229 return mate_in > 0 ? 10000 : -10000;
2230 }
2231
2232 // Parsing failed
2233 return fail_val;
2234 }
2235
2236 /*
2237 In find_pv_move we search for and move past " pv ".
2238 Then we handle a LAN move which will either be 4 or 5 chars and is followed by a space or possibly a newline.
2239 */
2240
2241 span find_pv_move(span line) {
2242 span pv_span = S(" pv ");
2243 span move;
2244
2245 // Search for " pv " in the line
2246 span pv_section = spanspan(line, pv_span);
2247 if (empty(pv_section)) return nullspan(); // " pv " not found
2248
2249 // Move past " pv "
2250 consume_prefix(&pv_section, pv_span);
2251
2252 // Handle LAN move: it will either be 4 or 5 characters long
2253 move.buf = pv_section.buf; // Start of the LAN move
2254 move.end = move.buf + 4; // Assume 4 characters initially
2255
2256 // Check if the move is actually 5 characters long (4 chars + ' ' or '\n')
2257 if (*(move.end) != ' ' && *(move.end) != '\n' && (move.end + 1) < pv_section.end) {
2258 move.end += 1; // Include the fifth character
2259 }
2260
2261 return move;
2262 }
2263
2264 /*
2265 Here we get a move and a LAN move as a span, along with the most recent eval from stockfish for that move.
2266
2267 We can do a linear scan over the move evaluations here as N is small.
2268 We simply check if the move is already in the list, and if it is, we update the cp eval.
2269 If not we add it and update the number of evals on the move.
2270 */
2271
2272 void update_or_add_eval(move *m, span lan_move, int cp_eval) {
2273 // Linear scan over existing move evaluations
2274 for (int i = 0; i < m->n_evals; ++i) {
2275 // Check if the current evaluation matches the LAN move
2276 if (span_eq(m->evals[i].lan_move, lan_move)) {
2277 // Update cp eval and return
2278 m->evals[i].cp_eval = cp_eval;
2279 return;
2280 }
2281 }
2282
2283 // If the move is not found in the existing evaluations, add a new evaluation
2284 if (m->n_evals < 128) { // Ensure we don't exceed the allocated space
2285 m->evals[m->n_evals].lan_move = lan_move;
2286 m->evals[m->n_evals].cp_eval = cp_eval;
2287 m->n_evals++; // Increment the count of evaluations
2288 } else {
2289 // Handle the unlikely case where there are more evaluations than expected
2290 prt("Error: Exceeded the maximum number of move evaluations (128).\n");
2291 flush();
2292 exit(EXIT_FAILURE);
2293 }
2294 }
2295
2296 /*
2297 Debugging helper function.
2298 */
2299
2300 void print_all_move_evals(Game *game) {
2301 for (int i = 0; i < game->move_count; ++i) {
2302 move current_move = game->moves[i];
2303 prt("Move %d: %.*s\n", i + 1, current_move.lan.end - current_move.lan.buf, current_move.lan.buf);
2304 prt("Legal Moves from this position:\n");
2305
2306 for (int j = 0; j < current_move.n_evals; ++j) {
2307 MoveEvaluation eval = current_move.evals[j];
2308 prt(" LAN Move: %.*s, CP Eval: %d\n", eval.lan_move.end - eval.lan_move.buf, eval.lan_move.buf, eval.cp_eval);
2309 }
2310 }
2311 }
2312
2313 void produce_output(Game *game);
2314
2315 /*
2316 To produce the PGN output, we already have a game object which contains, for each position reached in the game, the list of each legal move along with our eval from stockfish.
2317 First we iterate over the tags on the game object and reproduce them one per line, each enclosed in square brackets.
2318 Then we output a blank line (with terpri()) to indicate the tag section is ended.
2319
2320 Next we iterate over the moves, and output the move number starting with "1." and so on, followed by the SAN move which we already have, then a comment which we generate, which will contain arrows.
2321 An example PGN comment containing arrows is: { [%cal Gb8d7,Ga5b4,Gf6e4,Gf6h5,Gf6d7] }
2322 We generate the arrows by taking our move evals and interpreting them according to the following rules:
2323
2324 To determine whether the move is for white or black, we must use the loop iteration variable, as we do not store this information on the move struct explicitly.
2325 To get the move number we can also simply integer-divide the iteration counter by 2 and add one.
2326
2327 We find the highest cp_eval for any move in the position to determine if the position is winning, drawn, or losing.
2328 Then we draw arrows which are green for every legal move that maintains the win, if we consider it a win, or maintains the draw if we consider it a draw.
2329 If the position is losing, we will not draw any arrows, because all moves are equivalent in BPC terms in that case.
2330 We will draw red arrows for every move that changes from a win to a draw or a win to a loss.
2331
2332 We will convert between cp_eval numbers and categorical win, loss, or draw values according to thresholds, currently:
2333
2334 in range [-150, 150]: drawn
2335 greater than 150: winning
2336 less than -150: losing
2337
2338 Note that the cp_eval numbers are always in terms relative to the player who's turn it is, so we don't have to consider who has the move in interpreting these
2339 numbers.
2340
2341 Since our output just goes to stdout, we simply use prt() to generate our output PGN as described above.
2342 As usual, when we get invalid input or cannot proceed, we simply print a message (using prt() followed by flush()) and exit the process.
2343
2344 After each move (or half move) we just print a space, to keep the PGN output compact, as newlines are not required.
2345 We do include one newline at the end, though, so that our output ends with a newline as a text file always should.
2346
2347 (The _2 version fixes the order of the move arrows and some move number issue.)
2348 */
2349
2350 typedef enum { WINNING, DRAWN, LOSING } position_evaluation;
2351 position_evaluation evaluate_position(int cp_eval);
2352 void print_move_arrows(move *m);
2353
2354 void produce_output(Game *game) {
2355 // Iterate over the tags and reproduce them
2356 for (int i = 0; i < game->tag_count; ++i) {
2357 prt("[%.*s]\n", game->tags[i].end - game->tags[i].buf, game->tags[i].buf);
2358 }
2359 terpri();
2360
2361 // Iterate over the moves and output them
2362 for (int i = 0; i < game->move_count; ++i) {
2363 move *current_move = &game->moves[i];
2364 int move_number = (i / 2) + 1;
2365 if (i % 2 == 0) { // White's move
2366 prt("%d. ", move_number);
2367 } else { // Black's move, no move number needed, just a space
2368 prt(" ");
2369 }
2370
2371 // Output the SAN move
2372 prt("%.*s ", current_move->san.end - current_move->san.buf, current_move->san.buf);
2373
2374 // Generate and print arrows based on move evaluations
2375 print_move_arrows(current_move);
2376
2377 // Print a space after each move or half-move
2378 prt(" ");
2379 }
2380
2381 terpri();
2382 flush(); // Ensure all output is printed
2383 }
2384
2385 void produce_output_2(Game *game);
2386
2387 void produce_output_2(Game *game) {
2388 // Iterate over the tags and reproduce them
2389 for (int i = 0; i < game->tag_count; ++i) {
2390 prt("[%.*s]\n", game->tags[i].end - game->tags[i].buf, game->tags[i].buf);
2391 }
2392 terpri();
2393
2394 // Iterate over the moves and output them
2395 for (int i = 0; i < game->move_count; ++i) {
2396 move *current_move = &game->moves[i];
2397 int move_number = (i / 2) + 1;
2398
2399 // Generate and print arrows based on move evaluations
2400 print_move_arrows(current_move);
2401
2402 if (i % 2 == 0) { // White's move
2403 prt("%d. ", move_number);
2404 } else { // Black's move, no move number needed, just a space
2405 prt(" ");
2406 }
2407
2408 // Output the SAN move
2409 prt("%.*s ", current_move->san.end - current_move->san.buf, current_move->san.buf);
2410
2411 // Print a space after each move or half-move
2412 // actually don't need this I think -- Ed.
2413 //prt(" ");
2414 }
2415
2416 terpri();
2417 flush(); // Ensure all output is printed
2418 }
2419
2420 /*
2421 Here we implement our thresholding classification from the centipawn eval into our categorical win, loss, or draw classes.
2422 */
2423
2424 position_evaluation evaluate_position(int cp_eval) {
2425 if (cp_eval > 150) return WINNING;
2426 else if (cp_eval < -150) return LOSING;
2427 else return DRAWN;
2428 }
2429
2430 /*
2431 In print_move_arrows we are given a move and we must determine first what we consider the BPC (big-picture color) of the position to be.
2432 To do this we iterate over all the move evals in the position and find the highest cp_eval of any move.
2433 We call evaluate_position with this max value and store the result.
2434
2435 If the position is considered losing (i.e. the best move is classified as LOSING by our evaluation function) then we don't print any arrows, and just return.
2436 This is to avoid printing green arrows for each move in a losing position.
2437
2438 Then we iterate over each of the legal moves, i.e. each of the move_evals, and for each one we again get a categorical evaluation by calling evaluate_position.
2439 If the categorical eval is equal to the best possible one that we got for the highest-eval move, we consider this a non-error move and draw a green arrow.
2440 Otherwise we consider the move an error and draw a red arrow.
2441
2442 To "draw" the arrows, we output our PGN comment containing square brackets with the "%cal" tag.
2443 We must double the percent sign in the call to prt, as it uses the same format string syntax as printf.
2444 For each arrow, we have either "R" or "G" followed by the LAN move, which is already given on the move_eval.
2445 Concatenating the letter for the color with the LAN move consisting of the start and destination squares yields a valid arrow in the form that "%cal" uses.
2446 The arrows themselves are then separated by commas inside the whole "%cal" thing.
2447
2448 An example PGN comment containing arrows is: { [%cal Gb8d7,Ga5b4,Gf6e4,Gf6h5,Gf6d7,Rg6g7] }
2449 */
2450
2451 void print_move_arrows(move *m) {
2452 // Determine the best possible categorization (BPC) of the position
2453 int max_cp_eval = -10000; // Start with a very low value
2454 for (int i = 0; i < m->n_evals; ++i) {
2455 if (m->evals[i].cp_eval > max_cp_eval) {
2456 max_cp_eval = m->evals[i].cp_eval;
2457 }
2458 }
2459 position_evaluation bpc = evaluate_position(max_cp_eval);
2460
2461 if (bpc == LOSING) return; /* *** manual fixup *** */
2462
2463 // Start printing the comment containing arrows
2464 prt("{ [%%cal ");
2465
2466 for (int i = 0; i < m->n_evals; ++i) {
2467 position_evaluation move_eval = evaluate_position(m->evals[i].cp_eval);
2468
2469 // Determine arrow color based on comparison with BPC
2470 char arrow_color = (move_eval == bpc) ? 'G' : 'R'; // Green for maintaining BPC, Red for error
2471
2472 // "Draw" the arrow for the move
2473 prt("%c%.*s", arrow_color, m->evals[i].lan_move.end - m->evals[i].lan_move.buf, m->evals[i].lan_move.buf);
2474
2475 // Separate arrows with commas, but not after the last arrow
2476 if (i < m->n_evals - 1) {
2477 prt(",");
2478 }
2479 }
2480
2481 // Close the comment
2482 prt("] }");
2483 }
2484
2485 /*
2486 In print_positions(Game*) we print out each half-move as a number followed by one or three dots, a space, a SAN move, a FEN string, and a newline.
2487 This is mainly used to fetch FEN strings for any given position in a game for further use with Stockfish.
2488
2489 We assume that populate_lan_moves has been called first, but since the FEN strings aren't stored above, we call get_fen_from_stockfish() for each position.
2490 */
2491
2492 void print_positions(Game *game, StockfishProcess *sp);
2493
2494 void print_positions(Game *game, StockfishProcess *sp) {
2495 char fen[256]; // Buffer to hold FEN string
2496
2497 // Iterate over each move in the game
2498 for (int i = 0; i < game->move_count; ++i) {
2499 // Set the position in Stockfish up to this move
2500 send_position(sp, game, i + 1);
2501
2502 // Get the current position as FEN from Stockfish
2503 get_fen_from_stockfish(sp, fen, sizeof(fen));
2504
2505 // Print move number and dots
2506 if (i % 2 == 0) { // White's move
2507 prt("%d. ", (i / 2) + 1);
2508 } else { // Black's move
2509 prt("%d... ", (i / 2) + 1);
2510 }
2511
2512 // Print SAN move and FEN string
2513 prt("%.*s %s\n", game->moves[i].san.end - game->moves[i].san.buf, game->moves[i].san.buf, fen);
2514 }
2515 }
2516
2517 /*
2518 We have a global variable analysis_time_ms, and we want to be able to set this from the command line. Write a few lines here to handle argc and argv and update this variable if a corresponding flag is provided, otherwise we will leave it set to the default (which was already initialized above).
2519
2520 We also have just_print_fen which is a debugging feature, but we can support this with a command line flag as well, if the flag is present we can set this to 1 and we'll get the debugging output with the FEN position for each move.
2521
2522 We also have a debugging flag (global variable debug_mode) which we can turn on to print debugging information while parsing.
2523 (Despite the name, it is only relevant for the parsing phase.)
2524 This is useful if a PGN doesn't parse correctly.
2525
2526 We also have --help which prints a short usage summary, using prt(), flush(), and exit(0).
2527
2528 For the command-line flags we use "--analysis-time", "--just-print-fen", "--debug-parse", and of course "--help".
2529 */
2530
2531 int just_print_fen = 0;
2532
2533 void parse_command_line_arguments(int argc, char *argv[]) {
2534 for (int i = 1; i < argc; i++) { // Start from 1 to skip the program name
2535 if (strcmp(argv[i], "--analysis-time") == 0) {
2536 if (i + 1 < argc) { // Make sure there's another argument
2537 analysis_time_ms = atoi(argv[++i]); // Convert next argument to int and increment i
2538 }
2539 } else if (strcmp(argv[i], "--just-print-fen") == 0) {
2540 just_print_fen = 1; // Enable just print FEN mode
2541 } else if (strcmp(argv[i], "--debug-parse") == 0) {
2542 debug_mode = 1; // Enable debug mode for parsing
2543 } else if (strcmp(argv[i], "--help") == 0) {
2544 // Print usage information
2545 prt("Usage: %s [options]\n", argv[0]);
2546 prt("Options:\n");
2547 prt(" --analysis-time <ms> Set analysis time for Stockfish (in milliseconds)\n");
2548 prt(" --just-print-fen Print FEN strings for each move and exit\n");
2549 prt(" --debug-parse Enable debug output for PGN parsing\n");
2550 prt(" --help Display this help and exit\n");
2551 flush();
2552 exit(0);
2553 }
2554 }
2555 }
2556
2557 /*
2558 partly hand-written main() function as also used for debugging, testing partial code, etc.
2559 */
2560
2561 #define MAX_SPANS (1 << 20)
2562
2563 int main(int argc, char *argv[]) {
2564
2565 init_spans(); // Initialize your spans and buffers
2566 span_arena_alloc(MAX_SPANS);
2567
2568 parse_command_line_arguments(argc, argv);
2569
2570 read_and_count_stdin(); // Read the PGN data into the inp span
2571
2572 Game game = {0};
2573
2574 parse_pgn(&inp, &game);
2575
2576 // Print the moves from the parsed game
2577 //print_game(game);
2578
2579 // Rest of the main function, including Stockfish process handling
2580 StockfishProcess sp;
2581 launch_stockfish(&sp); // Launch and communicate with Stockfish
2582 // Send command to Stockfish, etc.
2583
2584 // Send command to Stockfish
2585 send_to_stockfish(&sp, "uci\n");
2586 send_to_stockfish(&sp, "setoption name MultiPV value 500\n");
2587 send_to_stockfish(&sp, "ucinewgame\n");
2588
2589 populate_lan_moves(&game, &sp);
2590
2591 if (just_print_fen) {
2592 // just print the FEN strings and moves for easier debugging via manual Stockfish input
2593 print_positions(&game, &sp);
2594 } else {
2595 // do the normal analysis
2596
2597 //dbgd(game.move_count);
2598 //for (int i=0;i < game.move_count;i++) {
2599 //wrs(game.moves[i].lan);terpri();
2600 //}
2601
2602 // Now we actually do the analysis, for each position reached.
2603 do_analysis(&game, &sp);
2604
2605 //print_all_move_evals(&game);
2606
2607 produce_output_2(&game);
2608 }
2609
2610 // Cleanup for Stockfish process
2611 close(sp.to_stockfish[1]);
2612 close(sp.from_stockfish[0]);
2613 waitpid(sp.pid, NULL, 0); // Wait for Stockfish to exit
2614
2615 flush(); // Ensure all output is written
2616 span_arena_free();
2617 return 0;
2618 }
2619