/** Runner framework for Project Euler solutions with benchmarking and validation. * * Header-only C analogue of `runner.py`. A solution source file includes this * header and implements `solve()`; the framework supplies `main()`, argument * parsing, benchmarking, and consistency checking. * * #include "runner.h" * * const char *solve(int argc, char *argv[]) { ... } * * Helpers are `static inline` so an including translation unit may use only the * ones it needs without tripping `-Werror` on unused functions. `main()` is * defined here, so each solution remains a single-source, single-executable * build (see scripts/c/compile.sh). * * Declarations follow the same order as runner.py: solve, get_text_file, show, * main, parse_int, parse_list. */#ifndef SOLVER_RUNNER_H#define SOLVER_RUNNER_H#include<libgen.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include<unistd.h>/** Solve the Project Euler problem; implemented by the solution source file. * * Returns the answer as a string so any result type (integer, big number, * concatenation, ...) is expressible. The returned pointer is owned by solve() * and must stay valid until the next call — return a string literal or write * into a `static` buffer; do not return a per-call malloc'd buffer (it would * leak across --runs). Return NULL to signal failure. * * @param argc Number of forwarded arguments (argv[0] is the program name). * @param argv Forwarded positional arguments with --runs= and --show removed. * @return The computed answer as a NUL-terminated string, or NULL on failure. */constchar*solve(intargc,char*argv[]);/* The invoking program path (argv[0]); set by main(), read by get_text_file(). * Mirrors runner.py resolving resources from argv[0] rather than the binary's own * location — directory-of-argv[0] is the solution dir regardless of cwd. */staticconstchar*runner_argv0="";/** Return the contents of a file from the 'resources' directory next to the solution. * * Resolves the path relative to the invoking program's directory (argv[0]'s parent), * looking in a sibling 'resources' subdirectory. Handles URLs by extracting the * filename. * * @param src File path or URL. Only the filename (after last '/') is used, and * query parameters (after '?') are stripped. * @return The complete file contents as a null-terminated string, or NULL on * error. The caller is responsible for freeing the returned buffer. */staticinlinechar*get_text_file(constchar*src){constchar*slash=strrchr(src,'/');constchar*name_start=slash?slash+1:src;constchar*q=strchr(name_start,'?');size_tname_len=q?(size_t)(q-name_start):strlen(name_start);charprog[4096];/* mutable copy — dirname() may modify its argument */if((size_t)snprintf(prog,sizeof(prog),"%s",runner_argv0)>=sizeof(prog))returnNULL;charpath[4096];intpn=snprintf(path,sizeof(path),"%s/resources/%.*s",dirname(prog),(int)name_len,name_start);if(pn<0||(size_t)pn>=sizeof(path))returnNULL;FILE*f=fopen(path,"rb");if(!f)returnNULL;if(fseek(f,0,SEEK_END)!=0){fclose(f);returnNULL;}longsz=ftell(f);if(sz<0){fclose(f);returnNULL;}rewind(f);char*buf=malloc((size_t)sz+1);if(!buf){fclose(f);returnNULL;}if(fread(buf,1,(size_t)sz,f)!=(size_t)sz){free(buf);fclose(f);returnNULL;}buf[sz]='\0';fclose(f);returnbuf;}/* Whether --show was passed; set by main(), read directly inside solve(). */staticintshow=0;/** Run the solver with benchmarking and consistency checking. * * Executes solve() one or more times, validates that results are consistent * across runs, and prints timing and result information. Errors go to stderr. * * Output format (stdout): "<runs> <avg_seconds> <result>". * * Command-line arguments: * <kwarg>...: Problem-specific parameters (forwarded to solve()). * --runs=N: Execute solve() N times (default 1) for benchmarking. * --show: Set the `show` flag for intermediate output inside solve(). * * @return 0 on success, 1 if results were inconsistent across runs. */intmain(intargc,char*argv[]){intruns=1;runner_argv0=argv[0];/* for get_text_file(): resolves resources/ next to the solution */char**solve_argv=malloc((size_t)argc*sizeof(char*));if(!solve_argv){fprintf(stderr,"runner: out of memory\n");return1;}intsolve_argc=0;solve_argv[solve_argc++]=argv[0];for(inti=1;i<argc;i++){if(argv[i][0]=='\0')continue;if(strncmp(argv[i],"--runs=",7)==0){intr=atoi(argv[i]+7);if(r>=1)runs=r;continue;}if(strcmp(argv[i],"--show")==0){show=1;continue;}solve_argv[solve_argc++]=argv[i];}char*result=NULL;/* strdup'd copy of the latest result, owned by the runner */doubletotal=0.0;intrc=0;for(intr=0;r<runs;r++){structtimespect0,t1;clock_gettime(CLOCK_MONOTONIC,&t0);constchar*cur=solve(solve_argc,solve_argv);/* <= timed call */clock_gettime(CLOCK_MONOTONIC,&t1);total+=(double)(t1.tv_sec-t0.tv_sec)+(double)(t1.tv_nsec-t0.tv_nsec)*1e-9;if(!cur){fprintf(stderr,"Expected a result, got NULL\n");rc=1;continue;}if(result&&strcmp(cur,result)!=0){fprintf(stderr,"Expected consistent result, got %s previous result=%s\n",cur,result);rc=1;}free(result);result=strdup(cur);}free(solve_argv);printf("%d %.17g %s\n",runs,total/(double)runs,result?result:"(null)");free(result);returnrc;}/** Parse an int given as a plain integer, power notation, or with underscores. * * @param token The argument string, e.g. "1000", "2**20", or "1_000_000". A "**" * separator marks integer exponentiation and any '_' digit * separators are ignored. * @return The parsed value as a long long integer. */staticinlinelonglongparse_int(constchar*token){charclean[64];size_tj=0;for(size_ti=0;token[i]!='\0'&&j+1<sizeof(clean);i++){if(token[i]!='_')clean[j++]=token[i];}clean[j]='\0';constchar*sep=strstr(clean,"**");if(sep){longlongbase=atoll(clean);longlongexp=atoll(sep+2);longlongresult=1;for(longlongi=0;i<exp;i++)result*=base;returnresult;}returnatoll(clean);}/** Parse a bracketed list of integers, e.g. "[1, 2, 3]" -> {1, 2, 3}. * * Brackets, whitespace, and commas are treated as separators; signed decimal * integers between them are collected. "[]" yields an empty list. * * @param token The list literal, e.g. "[1,2,3]" or "[-4, 5]". * @param count Out-parameter set to the number of parsed integers. * @return A malloc'd array of the parsed values, or NULL on allocation failure. * The caller is responsible for freeing the returned buffer. */staticinlinelonglong*parse_list(constchar*token,int*count){size_tcap=1;for(constchar*p=token;*p;p++)if(*p==',')cap++;longlong*out=malloc(cap*sizeof(longlong));intn=0;if(out){constchar*p=token;while(*p){while(*p&&*p!='-'&&*p!='+'&&!(*p>='0'&&*p<='9'))p++;if(!*p)break;char*end;longlongv=strtoll(p,&end,10);if(end==p)break;out[n++]=v;p=end;}}if(count)*count=n;returnout;}#endif /* SOLVER_RUNNER_H */