De-Vibe Coding: Examining Existing Code

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.

Directory Listing

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.

Lines 1-20

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.

Lines 21-500 (Library code)

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