From 91f65e8fdd7a613e88b51e827775300f5562890d Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Tue, 7 Jul 2026 16:55:18 +0000
Subject: [PATCH 1/2] tomlhosts: vendor tomlc17 parser

Imported from https://github.com/cktan/tomlc17 release R260618
(2026-06-18), upstream commit 7813bdd218b2b5f54940a9759ec0ffb3b60c1d1f.

The only local delta is the header include path (see LOCAL CHANGES
in tomlc17.c); MIT license preserved as src/common/tomlc17.LICENSE.
---
 src/common/Makefile          |    8 +
 src/common/meson.build       |   11 +
 src/common/tomlc17.LICENSE   |   22 +
 src/common/tomlc17.c         | 3170 ++++++++++++++++++++++++++++++++++
 src/include/common/tomlc17.h |  277 +++
 5 files changed, 3488 insertions(+)
 create mode 100644 src/common/tomlc17.LICENSE
 create mode 100644 src/common/tomlc17.c
 create mode 100644 src/include/common/tomlc17.h

diff --git a/src/common/Makefile b/src/common/Makefile
index 1a2fbbe887f..cb7aa292d78 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -79,6 +79,7 @@ OBJS_COMMON = \
 	scram-common.o \
 	string.o \
 	stringinfo.o \
+	tomlc17.o \
 	unicode_case.o \
 	unicode_category.o \
 	unicode_norm.o \
@@ -211,6 +212,13 @@ RYU_OBJS = $(RYU_FILES) $(RYU_FILES:%.o=%_shlib.o) $(RYU_FILES:%.o=%_srv.o)
 
 $(RYU_OBJS): CFLAGS += $(PERMIT_DECLARATION_AFTER_STATEMENT)
 
+# The vendored tomlc17 TOML parser also gets a pass on
+# declaration-after-statement, for the same reason as Ryu.
+TOMLC17_FILES = tomlc17.o
+TOMLC17_OBJS = $(TOMLC17_FILES) $(TOMLC17_FILES:%.o=%_shlib.o) $(TOMLC17_FILES:%.o=%_srv.o)
+
+$(TOMLC17_OBJS): CFLAGS += $(PERMIT_DECLARATION_AFTER_STATEMENT)
+
 clean distclean:
 	rm -f libpgcommon.a libpgcommon_shlib.a libpgcommon_srv.a libpgcommon_excluded_shlib.a
 	rm -f $(OBJS_FRONTEND) $(OBJS_SHLIB) $(OBJS_SRV)
diff --git a/src/common/meson.build b/src/common/meson.build
index 9bd55cda95b..b16f76b09db 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -74,6 +74,15 @@ ryu_cflags = []
 
 ryu_cflags += cflags_no_decl_after_statement
 
+# The vendored tomlc17 TOML parser also gets a pass on
+# declaration-after-statement, for the same reason as Ryu.
+tomlc17_sources = files(
+  'tomlc17.c',
+)
+tomlc17_cflags = []
+
+tomlc17_cflags += cflags_no_decl_after_statement
+
 config_info_sources = files('config_info.c',)
 config_info_cflags = [
   '-DVAL_CC="@0@"'.format(var_cc),
@@ -90,10 +99,12 @@ config_info_cflags = [
 # defined here.
 common_cflags = {
   'ryu': ryu_cflags,
+  'tomlc17': tomlc17_cflags,
   'config_info': config_info_cflags,
 }
 common_sources_cflags = {
   'ryu': ryu_sources,
+  'tomlc17': tomlc17_sources,
   'config_info': config_info_sources
 }
 
diff --git a/src/common/tomlc17.LICENSE b/src/common/tomlc17.LICENSE
new file mode 100644
index 00000000000..cd2995d5535
--- /dev/null
+++ b/src/common/tomlc17.LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2024-2026, CK Tan
+https://github.com/cktan/tomlc17
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/common/tomlc17.c b/src/common/tomlc17.c
new file mode 100644
index 00000000000..d9ab403f92e
--- /dev/null
+++ b/src/common/tomlc17.c
@@ -0,0 +1,3170 @@
+/*-------------------------------------------------------------------------
+ *
+ * tomlc17.c
+ *	  Vendored TOML 1.0/1.1 parser from https://github.com/cktan/tomlc17
+ *
+ * Imported verbatim from upstream commit
+ *	 7813bdd218b2b5f54940a9759ec0ffb3b60c1d1f
+ * with the single local modification documented below. Re-vendoring
+ * must re-apply this delta or the build will fail.
+ *
+ * LOCAL CHANGES:
+ *	 - The original `#include "tomlc17.h"` was rewritten to
+ *	   `#include "common/tomlc17.h"` so PostgreSQL's standard
+ *	   `src/common/` -> `src/include/common/` layout resolves the
+ *	   header without per-file include flags. The PG-local marker
+ *	   comment below the upstream copyright tags the exact site.
+ *
+ *-------------------------------------------------------------------------
+ */
+/* Copyright (c) 2024-2026, CK Tan.
+ * https://github.com/cktan/tomlc17/blob/main/LICENSE
+ */
+/* PG local: was #include "tomlc17.h" - re-apply on re-vendor */
+#include "common/tomlc17.h"
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <math.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+const toml_datum_t DATUM_ZERO = {0};
+
+static toml_option_t toml_option = {0, realloc, free};
+
+#define MALLOC(n) toml_option.mem_realloc(0, n)
+#define REALLOC(p, n) toml_option.mem_realloc(p, n)
+#define FREE(p) toml_option.mem_free(p)
+
+#define DO(x)                                                                  \
+  if (x)                                                                       \
+    return -1;                                                                 \
+  else                                                                         \
+    (void)0
+
+// Copy string src to dst where dst is limited to dstsz that includes
+// NUL. Return 0 on success, -1 otherwise (because src[] is longer than dst[]).
+static inline int copystring(char *dst, int dstsz, const char *src) {
+  int srcsz = strlen(src) + 1;
+  if (srcsz > dstsz) {
+    return -1;
+  }
+  memcpy(dst, src, srcsz);
+  return 0;
+}
+
+/*
+ *  Error buffer
+ */
+typedef struct ebuf_t ebuf_t;
+struct ebuf_t {
+  char *ptr;
+  int len;
+};
+
+/*
+ *  Format an error into ebuf[]. Always return -1.
+ */
+static int SETERROR(ebuf_t ebuf, int lineno, const char *fmt, ...) {
+  va_list args;
+  va_start(args, fmt);
+  char *p = ebuf.ptr;
+  char *q = p + ebuf.len;
+  if (lineno) {
+    snprintf(p, p < q ? q - p : 0, "(line %d) ", lineno);
+    p += strlen(p);
+  }
+  vsnprintf(p, p < q ? q - p : 0, fmt, args);
+  va_end(args);
+  return -1;
+}
+
+/*
+ *  Memory pool. Allocated a big block once and hand out piecemeal.
+ */
+typedef struct pool_t pool_t;
+struct pool_t {
+  int max;     // size of buf[]
+  int top;     // offset of first free byte in buf[]
+  char buf[1]; // first byte starts here
+};
+
+/**
+ *  Create a memory pool of N bytes. Return the memory pool on
+ *  success, or NULL if out of memory.
+ */
+static pool_t *pool_create(int N) {
+  if (N <= 0) {
+    N = 100; // minimum
+  }
+  int totalsz = sizeof(pool_t) + N;
+  pool_t *pool = MALLOC(totalsz);
+  if (!pool) {
+    return NULL;
+  }
+  memset(pool, 0, totalsz);
+  pool->max = N;
+  return pool;
+}
+
+/**
+ *  Destroy a memory pool.
+ */
+static void pool_destroy(pool_t *pool) { FREE(pool); }
+
+/**
+ *  Allocate n bytes from pool. Return the memory allocated on
+ *  success, or NULL if out of memory.
+ */
+static char *pool_alloc(pool_t *pool, int n) {
+  if (pool->top + n > pool->max) {
+    return NULL;
+  }
+  char *ret = pool->buf + pool->top;
+  pool->top += n;
+  return ret;
+}
+
+// Cell: expandable buffer that behaves like realloc
+typedef struct cell_t cell_t;
+struct cell_t {
+  uint32_t top, max;
+  // first byte of data starts here (i.e. &(*this)[1])
+};
+
+static char *cell_realloc(char *p, int size) {
+  assert(size >= 0);
+  if (p == NULL) {
+    // first malloc
+    cell_t *tmp = REALLOC(NULL, size + sizeof(cell_t));
+    if (!tmp) {
+      return 0; // out of memory
+    }
+    cell_t *cp = tmp;
+    cp->max = size;
+    cp->top = size;
+    return (char *)&cp[1];
+  }
+
+  // obtain a handle to cell info
+  cell_t *cp = (cell_t *)(p - sizeof(cell_t));
+  if ((uint32_t)size <= cp->max) {
+    // cell is big enough for the new size. DONE.
+    cp->top = size;
+    return p;
+  }
+
+  // need to expand. add 30% margin.
+  int newmax = size * 1.3 + 100;
+  cell_t *tmp = REALLOC(cp, newmax + sizeof(cell_t));
+  if (!tmp) {
+    return 0; // out of memory
+  }
+  cp = tmp;
+  cp->max = newmax;
+  cp->top = size;
+  return (char *)&cp[1];
+}
+
+static void cell_free(char *p) {
+  if (p) {
+    cell_t *cp = (cell_t *)(p - sizeof(cell_t));
+    FREE(cp);
+  }
+}
+
+/* This is a string view. */
+typedef struct span_t span_t;
+struct span_t {
+  const char *ptr;
+  int len;
+};
+
+/* Represents a multi-part key */
+#define KEYPARTMAX 10
+typedef struct keypart_t keypart_t;
+struct keypart_t {
+  int nspan;
+  span_t span[KEYPARTMAX];
+};
+
+static int utf8_to_ucs(const char *s, int len, uint32_t *ret);
+static int ucs_to_utf8(uint32_t code, char buf[4]);
+
+// flags for toml_datum_t::flag.
+#define FLAG_INLINED 1
+#define FLAG_STDEXPR 2
+#define FLAG_EXPLICIT 4
+
+// Maximum levels of brackets and braces to prevent
+// stack overflow during recursive descent of the parser.
+#define BRACKET_LEVEL_MAX 30
+#define BRACE_LEVEL_MAX 30
+#define TABLE_MAX (1 << 14) // 16k
+#define ARRAY_MAX (1 << 14) // 16k
+
+enum toktyp_t {
+  TOK_DOT = 1,
+  TOK_EQUAL,
+  TOK_COMMA,
+  TOK_LBRACK,  // [
+  TOK_LLBRACK, // [[
+  TOK_RBRACK,  // ]
+  TOK_RRBRACK, // ]]
+  TOK_LBRACE,  // {
+  TOK_RBRACE,  // }
+  TOK_LIT,
+  TOK_STRING,      // "string"
+  TOK_MLSTRING,    // """multi-line-string"""
+  TOK_LITSTRING,   // 'lit-string'
+  TOK_MLLITSTRING, // '''multi-line-lit-string'''
+  TOK_TIME,
+  TOK_DATE,
+  TOK_DATETIME,
+  TOK_DATETIMETZ,
+  TOK_INTEGER,
+  TOK_FLOAT,
+  TOK_BOOL,
+  TOK_ENDL,
+  TOK_FIN = -5000, // EOF
+};
+typedef enum toktyp_t toktyp_t;
+typedef struct scanner_t scanner_t;
+
+/* Remember the current state of a scanner */
+typedef struct scanner_state_t scanner_state_t;
+struct scanner_state_t {
+  scanner_t *sp;
+  const char *cur; // points into scanner_t::src[]
+  int lineno;      // current line number
+  const char *line_start;
+};
+
+// A scan token
+typedef struct token_t token_t;
+struct token_t {
+  toktyp_t toktyp;
+  int lineno;
+  int colno;
+  span_t str;
+
+  // values represented by str
+  union {
+    const char *escp; // point to an esc char in str
+    int64_t int64;
+    double fp64;
+    bool b1;
+    struct {
+      // validity depends on toktyp for TIME, DATE, DATETIME, DATETIMETZ
+      int year, month, day, hour, minute, sec, usec;
+      int tz; // +- minutes
+    } tsval;
+  } u;
+};
+
+// Scanner object
+struct scanner_t {
+  const char *src;  // src[] is a NUL-terminated string
+  const char *endp; // end of src[]. always pointing at a NUL char.
+  const char *cur;  // current char in src[]
+  int lineno;       // line number of current char
+  const char *line_start;
+  char *errmsg; // set to ebuf.ptr if there was an error
+  ebuf_t ebuf;  // buffer to store error message
+
+  int bracket_level; // count depth of [ ]
+  int brace_level;   // count depth of { }
+};
+static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf,
+                      int errbufsz);
+static int scan_key(scanner_t *sp, token_t *tok);
+static int scan_value(scanner_t *sp, token_t *tok);
+// restore scanner to state before tok was returned
+static scanner_state_t scan_mark(scanner_t *sp);
+static void scan_restore(scanner_t *sp, scanner_state_t state);
+
+#ifndef min
+static inline int min(int a, int b) { return a < b ? a : b; }
+#endif
+
+// Copy up to dstsz - 1 chars from the current position of the scanner
+// to dst, and always terminate dst[] with a NUL if dstsz > 0.
+static void scan_copystr(scanner_t *sp, char *dst, int dstsz) {
+  assert(dstsz > 0);
+  int len = min(sp->endp - sp->cur, dstsz - 1); // account for NUL
+  if (len > 0) {
+    memcpy(dst, sp->cur, len);
+    dst[len] = '\0';
+  }
+}
+
+// Parser object
+typedef struct parser_t parser_t;
+struct parser_t {
+  scanner_t scanner;
+  toml_datum_t toptab;  // top table
+  toml_datum_t *curtab; // current table
+  pool_t *pool;         // memory pool for strings
+  const char *srcname;  // source name copied into the pool, or NULL
+  ebuf_t ebuf;          // buffer to store last error message
+};
+
+// Find key in tab and return its index. If not found, return -1.
+static int tab_find(toml_datum_t *tab, span_t key) {
+  assert(tab->type == TOML_TABLE);
+  for (int i = 0, top = tab->u.tab.size; i < top; i++) {
+    if (tab->u.tab.len[i] == key.len &&
+        0 == memcmp(tab->u.tab.key[i], key.ptr, key.len)) {
+      return i;
+    }
+  }
+  return -1;
+}
+
+// Put key into tab dictionary. Return a pointer to the datum for the key.
+// If the key already exists, returns a pointer to its existing datum (the
+// datum is NOT zeroed — callers must check datum->type to detect duplicates).
+// If the key is new, appends it with a zero datum and returns a pointer to it.
+// Returns NULL on allocation failure.
+//
+// Dual-use: tab_add() uses the non-zero type to detect and reject duplicates;
+// datum_copy/datum_merge() use it to locate or create a slot for writing.
+static toml_datum_t *tab_emplace(toml_datum_t *tab, span_t key,
+                                 const char **reason) {
+  assert(tab->type == TOML_TABLE);
+  int i = tab_find(tab, key);
+  if (i >= 0) {
+    return &tab->u.tab.value[i];
+  }
+
+  // Expand pkey[], plen[] and value[].
+  int N = tab->u.tab.size;
+  if (N >= TABLE_MAX) {
+    *reason = "table too large";
+    return NULL;
+  }
+
+  {
+    char **pkey = (char **)cell_realloc((char *)(void *)tab->u.tab.key,
+                                        sizeof(*pkey) * (N + 1));
+    int *plen =
+        (int *)cell_realloc((char *)tab->u.tab.len, sizeof(*plen) * (N + 1));
+    toml_datum_t *value = (toml_datum_t *)cell_realloc(
+        (char *)tab->u.tab.value, sizeof(*value) * (N + 1));
+
+    // on success, must save new pointers in tab->u.tab because the
+    // old memory areas are gone.
+    if (pkey) {
+      tab->u.tab.key = (const char **)pkey;
+    }
+    if (plen) {
+      tab->u.tab.len = plen;
+    }
+    if (value) {
+      tab->u.tab.value = value;
+    }
+
+    // if any fail, it is safe to bail out.
+    if (!pkey || !plen || !value) {
+      *reason = "out of memory";
+      return NULL;
+    }
+  }
+
+  // There is sufficient space in all the arrays for one more element.
+  // Append the new key. The value is set to DATUM_ZERO. Caller will
+  // overwrite with a valid datum.
+  tab->u.tab.size = N + 1;
+  tab->u.tab.key[N] = (char *)key.ptr;
+  tab->u.tab.len[N] = key.len;
+  tab->u.tab.value[N] = DATUM_ZERO;
+  return &tab->u.tab.value[N];
+}
+
+// Add a new key in tab. Return 0 on success, -1 otherwise.
+// On error, *reason will point to an error message.
+static int tab_add(toml_datum_t *tab, span_t newkey, toml_datum_t newvalue,
+                   const char **reason) {
+  assert(tab->type == TOML_TABLE);
+  toml_datum_t *pvalue = tab_emplace(tab, newkey, reason);
+  if (!pvalue) {
+    return -1;
+  }
+  if (pvalue->type) {
+    *reason = "duplicate key";
+    return -1;
+  }
+  *pvalue = newvalue;
+  return 0;
+}
+
+// Add a new element into an array. Return 0 on success, -1 otherwise.
+// On error, *reason will point to an error message.
+static toml_datum_t *arr_emplace(toml_datum_t *arr, const char **reason) {
+  assert(arr->type == TOML_ARRAY);
+  int n = arr->u.arr.size;
+  if (n >= ARRAY_MAX) {
+    *reason = "array too large";
+    return NULL;
+  }
+  toml_datum_t *elem = (toml_datum_t *)cell_realloc((char *)arr->u.arr.elem,
+                                                    sizeof(*elem) * (n + 1));
+  if (!elem) {
+    *reason = "out of memory";
+    return NULL;
+  }
+  arr->u.arr.elem = elem;
+  arr->u.arr.size = n + 1;
+  elem[n] = DATUM_ZERO;
+  return &elem[n];
+}
+
+// ------------------- parser section
+static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span);
+static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret);
+static int parse_keyvalue_expr(parser_t *pp, token_t tok);
+static int parse_std_table_expr(parser_t *pp, token_t tok);
+static int parse_array_table_expr(parser_t *pp, token_t tok);
+
+static toml_datum_t mkdatum(toml_type_t ty) {
+  toml_datum_t ret = {0};
+  ret.type = ty;
+  if (ty == TOML_DATE || ty == TOML_TIME || ty == TOML_DATETIME ||
+      ty == TOML_DATETIMETZ) {
+    ret.u.ts.year = -1;
+    ret.u.ts.month = -1;
+    ret.u.ts.day = -1;
+    ret.u.ts.hour = -1;
+    ret.u.ts.minute = -1;
+    ret.u.ts.second = -1;
+    ret.u.ts.usec = -1;
+    ret.u.ts.tz = -1;
+  }
+  return ret;
+}
+
+static toml_datum_t mkdatum_at(toml_type_t ty, int lineno, int colno) {
+  toml_datum_t ret = mkdatum(ty);
+  ret.lineno = lineno;
+  ret.colno = colno;
+  return ret;
+}
+
+// Recursively free any dynamically allocated memory in the datum tree
+static void datum_free(toml_datum_t *datum) {
+  if (datum->type == TOML_TABLE) {
+    for (int i = 0, top = datum->u.tab.size; i < top; i++) {
+      datum_free(&datum->u.tab.value[i]);
+    }
+    cell_free((char *)(void *)datum->u.tab.key);
+    cell_free((char *)datum->u.tab.len);
+    cell_free((char *)datum->u.tab.value);
+  } else if (datum->type == TOML_ARRAY) {
+    for (int i = 0, top = datum->u.arr.size; i < top; i++) {
+      datum_free(&datum->u.arr.elem[i]);
+    }
+    cell_free((char *)datum->u.arr.elem);
+  }
+  // other types do not allocate memory
+  *datum = DATUM_ZERO;
+}
+
+// Maps each distinct source-name pointer to a single copy in the destination
+// pool (deduplicated), so the merged pool holds at most one copy of each source
+// name (keeping it within the r1pool->top + r2pool->top budget).
+typedef struct srcmap_t srcmap_t;
+struct srcmap_t {
+  pool_t *pool;
+  const char **olds; // cell buffer of old pointers
+  const char **news; // cell buffer of new (copied) pointers
+  int n, cap;
+};
+
+// Returns the deduplicated copy of src, or NULL for a NULL input or on
+// out-of-memory. Callers distinguish the two: a non-NULL src that yields NULL
+// is a hard failure.
+static const char *dedup_source(srcmap_t *m, const char *src) {
+  if (!src) {
+    return NULL;
+  }
+  for (int i = 0; i < m->n; i++) {
+    if (m->olds[i] == src) {
+      return m->news[i];
+    }
+  }
+  // Grow the memo first, so an allocation failure aborts cleanly.
+  if (m->n == m->cap) {
+    int newcap = m->cap ? m->cap * 2 : 4;
+    const char **no = (const char **)cell_realloc((char *)(void *)m->olds,
+                                                  sizeof(*no) * newcap);
+    if (!no) {
+      return NULL; // out of memory
+    }
+    m->olds = no;
+    const char **nn = (const char **)cell_realloc((char *)(void *)m->news,
+                                                  sizeof(*nn) * newcap);
+    if (!nn) {
+      return NULL; // out of memory
+    }
+    m->news = nn;
+    m->cap = newcap;
+  }
+  int len = (int)strlen(src) + 1;
+  char *p = pool_alloc(m->pool, len);
+  if (!p) {
+    return NULL; // out of memory
+  }
+  memcpy(p, src, len);
+  m->olds[m->n] = src;
+  m->news[m->n] = p;
+  m->n++;
+  return p;
+}
+
+// Make a deep copy of src to dst.
+// Return 0 on success, -1 otherwise.
+static int datum_copy(toml_datum_t *dst, toml_datum_t src, srcmap_t *sm,
+                      const char **reason) {
+  *dst = mkdatum_at(src.type, src.lineno, src.colno);
+  dst->flag = src.flag;
+  switch (src.type) {
+  case TOML_STRING:
+    dst->u.str.ptr = pool_alloc(sm->pool, src.u.str.len + 1);
+    if (!dst->u.str.ptr) {
+      *reason = "out of memory";
+      goto bail;
+    }
+    dst->u.str.len = src.u.str.len;
+    memcpy((char *)dst->u.str.ptr, src.u.str.ptr, src.u.str.len + 1);
+    break;
+  case TOML_TABLE:
+    for (int i = 0; i < src.u.tab.size; i++) {
+      char *keycopy = pool_alloc(sm->pool, src.u.tab.len[i] + 1);
+      if (!keycopy) {
+        *reason = "out of memory";
+        goto bail;
+      }
+      memcpy(keycopy, src.u.tab.key[i], src.u.tab.len[i] + 1);
+      span_t newkey = {keycopy, src.u.tab.len[i]};
+      toml_datum_t *pvalue = tab_emplace(dst, newkey, reason);
+      if (!pvalue) {
+        goto bail;
+      }
+      if (datum_copy(pvalue, src.u.tab.value[i], sm, reason)) {
+        goto bail;
+      }
+    }
+    break;
+  case TOML_ARRAY:
+    for (int i = 0; i < src.u.arr.size; i++) {
+      toml_datum_t *pelem = arr_emplace(dst, reason);
+      if (!pelem) {
+        goto bail;
+      }
+      if (datum_copy(pelem, src.u.arr.elem[i], sm, reason)) {
+        goto bail;
+      }
+    }
+    break;
+  default:
+    *dst = src;
+    break;
+  }
+
+  dst->source = dedup_source(sm, src.source);
+  if (src.source && !dst->source) {
+    *reason = "out of memory";
+    goto bail;
+  }
+  return 0;
+
+bail:
+  datum_free(dst);
+  return -1;
+}
+
+// Check if datum is an array of tables.
+static inline bool is_array_of_tables(toml_datum_t datum) {
+  bool ret = (datum.type == TOML_ARRAY);
+  for (int i = 0; ret && i < datum.u.arr.size; i++) {
+    ret = (datum.u.arr.elem[i].type == TOML_TABLE);
+  }
+  return ret;
+}
+
+// Merge src into dst. Return 0 on success, -1 otherwise.
+static int datum_merge(toml_datum_t *dst, toml_datum_t src, srcmap_t *sm,
+                       const char **reason) {
+  if (dst->type != src.type) {
+    datum_free(dst);
+    return datum_copy(dst, src, sm, reason);
+  }
+  switch (src.type) {
+  case TOML_TABLE:
+    // for key-value in src:
+    //    override key-value in dst.
+    for (int i = 0; i < src.u.tab.size; i++) {
+      char *keycopy = pool_alloc(sm->pool, src.u.tab.len[i] + 1);
+      if (!keycopy) {
+        *reason = "out of memory";
+        return -1;
+      }
+      memcpy(keycopy, src.u.tab.key[i], src.u.tab.len[i] + 1);
+      span_t key;
+      key.ptr = keycopy;
+      key.len = src.u.tab.len[i];
+      toml_datum_t *pvalue = tab_emplace(dst, key, reason);
+      if (!pvalue) {
+        return -1;
+      }
+      if (pvalue->type) {
+        DO(datum_merge(pvalue, src.u.tab.value[i], sm, reason));
+      } else {
+        datum_free(pvalue);
+        DO(datum_copy(pvalue, src.u.tab.value[i], sm, reason));
+      }
+    }
+    return 0;
+  case TOML_ARRAY:
+    if (is_array_of_tables(src)) {
+      // append src array to dst
+      for (int i = 0; i < src.u.arr.size; i++) {
+        toml_datum_t *pelem = arr_emplace(dst, reason);
+        if (!pelem) {
+          return -1;
+        }
+        DO(datum_copy(pelem, src.u.arr.elem[i], sm, reason));
+      }
+      return 0;
+    }
+    // fallthru
+  default:
+    break;
+  }
+  datum_free(dst);
+  return datum_copy(dst, src, sm, reason);
+}
+
+// Compare the content of a and b.
+static bool datum_equiv(toml_datum_t a, toml_datum_t b) {
+  if (a.type != b.type) {
+    return false;
+  }
+  int N;
+  switch (a.type) {
+  case TOML_STRING:
+    return a.u.str.len == b.u.str.len &&
+           0 == memcmp(a.u.str.ptr, b.u.str.ptr, a.u.str.len);
+  case TOML_INT64:
+    return a.u.int64 == b.u.int64;
+  case TOML_FP64:
+    return a.u.fp64 == b.u.fp64 || (isnan(a.u.fp64) && isnan(b.u.fp64));
+  case TOML_BOOLEAN:
+    return !!a.u.boolean == !!b.u.boolean;
+  case TOML_DATE:
+    return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month &&
+           a.u.ts.day == b.u.ts.day;
+  case TOML_TIME:
+    return a.u.ts.hour == b.u.ts.hour && a.u.ts.minute == b.u.ts.minute &&
+           a.u.ts.second == b.u.ts.second && a.u.ts.usec == b.u.ts.usec;
+  case TOML_DATETIME:
+    return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month &&
+           a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour &&
+           a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second &&
+           a.u.ts.usec == b.u.ts.usec;
+  case TOML_DATETIMETZ:
+    return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month &&
+           a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour &&
+           a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second &&
+           a.u.ts.usec == b.u.ts.usec && a.u.ts.tz == b.u.ts.tz;
+  case TOML_ARRAY:
+    N = a.u.arr.size;
+    if (N != b.u.arr.size) {
+      return false;
+    }
+    for (int i = 0; i < N; i++) {
+      if (!datum_equiv(a.u.arr.elem[i], b.u.arr.elem[i])) {
+        return false;
+      }
+    }
+    return true;
+  case TOML_TABLE:
+    N = a.u.tab.size;
+    if (N != b.u.tab.size) {
+      return false;
+    }
+    for (int i = 0; i < N; i++) {
+      int len = a.u.tab.len[i];
+      if (len != b.u.tab.len[i]) {
+        return false;
+      }
+      if (0 != memcmp(a.u.tab.key[i], b.u.tab.key[i], len)) {
+        return false;
+      }
+      if (!datum_equiv(a.u.tab.value[i], b.u.tab.value[i])) {
+        return false;
+      }
+    }
+    return true;
+  default:
+    break;
+  }
+  return false;
+}
+
+/**
+ *  Override values in r1 using r2. Return a new result. All results
+ *  (i.e., r1, r2 and the returned result) must be freed using toml_free()
+ *  after use.
+ *
+ *  LOGIC:
+ *   ret = copy of r1
+ *   for each item x in r2:
+ *     if x is not in ret:
+ *          override
+ *     elif x in ret is NOT of the same type:
+ *         override
+ *     elif x is an array of tables:
+ *         append r2.x to ret.x
+ *     elif x is a table:
+ *         merge r2.x to ret.x
+ *     else:
+ *         override
+ */
+toml_result_t toml_merge(const toml_result_t *r1, const toml_result_t *r2) {
+  const char *reason = "";
+  toml_result_t ret = {0};
+  pool_t *pool = 0;
+  srcmap_t sm = {0};
+  if (!r1->ok) {
+    reason = "param error: r1 not ok";
+    goto bail;
+  }
+  if (!r2->ok) {
+    reason = "param error: r2 not ok";
+    goto bail;
+  }
+  {
+    pool_t *r1pool = (pool_t *)r1->__internal;
+    pool_t *r2pool = (pool_t *)r2->__internal;
+    pool = pool_create(r1pool->top + r2pool->top);
+    if (!pool) {
+      reason = "out of memory";
+      goto bail;
+    }
+  }
+  sm.pool = pool;
+
+  // Make a copy of r1
+  if (datum_copy(&ret.toptab, r1->toptab, &sm, &reason)) {
+    goto bail;
+  }
+
+  // Merge r2 into the result
+  if (datum_merge(&ret.toptab, r2->toptab, &sm, &reason)) {
+    goto bail;
+  }
+
+  cell_free((char *)(void *)sm.olds);
+  cell_free((char *)(void *)sm.news);
+  ret.ok = 1;
+  ret.__internal = pool;
+  return ret;
+
+bail:
+  cell_free((char *)(void *)sm.olds);
+  cell_free((char *)(void *)sm.news);
+  pool_destroy(pool);
+  snprintf(ret.errmsg, sizeof(ret.errmsg), "%s", reason);
+  return ret;
+}
+
+bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2) {
+  if (!(r1->ok && r2->ok)) {
+    return false;
+  }
+  return datum_equiv(r1->toptab, r2->toptab);
+}
+
+/**
+ * Find a key in a toml_table. Return the value of the key if found,
+ * or a TOML_UNKNOWN otherwise.
+ */
+toml_datum_t toml_get(toml_datum_t datum, const char *key) {
+  if (datum.type == TOML_TABLE) {
+    int n = datum.u.tab.size;
+    if (n > 0) {
+      int key_len = strlen(key);
+      const char **pkey = datum.u.tab.key;
+      int *plen = datum.u.tab.len;
+      toml_datum_t *pvalue = datum.u.tab.value;
+      for (int i = 0; i < n; i++) {
+        if (plen[i] == key_len && 0 == memcmp(pkey[i], key, key_len)) {
+          return pvalue[i];
+        }
+      }
+    }
+  }
+  return DATUM_ZERO;
+}
+
+/**
+ * Locate a value starting from a toml_table. Return the value of the key if
+ * found, or a TOML_UNKNOWN otherwise.
+ *
+ * Note: the multipart-key is separated by DOT, and must not have any escape
+ * chars.
+ */
+toml_datum_t toml_seek(toml_datum_t table, const char *multipart_key) {
+  if (table.type != TOML_TABLE) {
+    return DATUM_ZERO;
+  }
+
+  // Make a mutable copy of the multipart_key for splitting
+  char buf[256];
+  if (copystring(buf, sizeof(buf), multipart_key)) {
+    // if the multipart_key is longer than buffer, just
+    // signal a not-found.
+    return DATUM_ZERO;
+  }
+
+  // Go through the multipart name part by part.
+  char *p = buf;
+  toml_datum_t datum = table;
+  while (datum.type == TOML_TABLE) {
+    char *q = strchr(p, '.');
+    if (q) {
+      // traverse to next key
+      *q = 0;
+      datum = toml_get(datum, p);
+      p = q + 1;
+      continue;
+    }
+
+    // At end of last keypart.
+    // look up p in the final table
+    return toml_get(datum, p);
+  }
+
+  return DATUM_ZERO;
+}
+
+/**
+ *  Return the default options.
+ */
+toml_option_t toml_default_option(void) {
+  toml_option_t opt = {0, realloc, free};
+  return opt;
+}
+
+/**
+ *  Override the current options.
+ */
+void toml_set_option(toml_option_t opt) { toml_option = opt; }
+
+/**
+ *  Free the result returned by toml_parse().
+ */
+void toml_free(toml_result_t result) {
+  datum_free(&result.toptab);
+  pool_destroy((pool_t *)result.__internal);
+}
+
+/**
+ *  Parse a toml document.
+ */
+toml_result_t toml_parse_file_ex(const char *fname) {
+  toml_result_t result = {0};
+  FILE *fp = fopen(fname, "r");
+  if (!fp) {
+    snprintf(result.errmsg, sizeof(result.errmsg), "fopen %s: %s", fname,
+             strerror(errno));
+    return result;
+  }
+  result = toml_parse_file_named(fp, fname);
+  fclose(fp);
+  return result;
+}
+
+toml_result_t toml_parse_file(FILE *fp) {
+  return toml_parse_file_named(fp, NULL);
+}
+
+/**
+ *  Parse a toml document from a file, tagging datums with name.
+ */
+toml_result_t toml_parse_file_named(FILE *fp, const char *name) {
+  toml_result_t result = {0};
+  if (!fp) {
+    snprintf(result.errmsg, sizeof(result.errmsg), "fp is NULL");
+    return result;
+  }
+  char *buf = 0;
+  int top = 0;                 // number of bytes read into buf[]
+  enum { CHUNKSZ = 8 * 1024 }; // bytes to read per iteration
+
+  // Read file into memory. cell_realloc handles capacity growth, so we only
+  // need to ask for room for one more chunk (plus a terminating NUL) each pass.
+  // Drive the loop off fread's return value rather than feof(): feof() only
+  // reports true after a read has already hit EOF, and this also guarantees buf
+  // is allocated at least once before the NUL terminator below.
+  for (;;) {
+    if (top > INT_MAX - CHUNKSZ - 1) {
+      snprintf(result.errmsg, sizeof(result.errmsg), "file is too big");
+      break;
+    }
+    // add 1 to CHUNKSZ so we always have room for terminating NUL.
+    char *tmp = cell_realloc(buf, top + CHUNKSZ + 1);
+    if (!tmp) {
+      snprintf(result.errmsg, sizeof(result.errmsg), "out of memory");
+      break;
+    }
+    buf = tmp;
+
+    errno = 0;
+    size_t n = fread(buf + top, 1, CHUNKSZ, fp);
+    top += n;
+    if (n < CHUNKSZ) { // short read => EOF or error
+      if (ferror(fp)) {
+        snprintf(result.errmsg, sizeof(result.errmsg), "%s",
+                 errno ? strerror(errno) : "Error reading file");
+        break;
+      }
+      if (feof(fp)) {
+        break;
+      }
+      // small probability of short read due to signal, etc.
+    }
+  }
+  // error?
+  if (result.errmsg[0]) {
+    cell_free(buf);
+    return result;
+  }
+  buf[top] = 0; // NUL terminator
+
+  result = toml_parse_named(buf, top, name);
+  cell_free(buf);
+  return result;
+}
+
+static void set_source_recursive(toml_datum_t *datum, const char *source) {
+  datum->source = source;
+  switch (datum->type) {
+  case TOML_ARRAY:
+    for (int i = 0, top = datum->u.arr.size; i < top; i++) {
+      set_source_recursive(&datum->u.arr.elem[i], source);
+    }
+    break;
+  case TOML_TABLE:
+    for (int i = 0, top = datum->u.tab.size; i < top; i++) {
+      set_source_recursive(&datum->u.tab.value[i], source);
+    }
+    break;
+  default:
+    break;
+  }
+}
+
+/**
+ *  Parse a toml document.
+ */
+toml_result_t toml_parse(const char *src, int len) {
+  return toml_parse_named(src, len, NULL);
+}
+
+toml_result_t toml_parse_named(const char *src, int len, const char *name) {
+  toml_result_t result = {0};
+  parser_t parser = {0};
+  parser_t *pp = &parser;
+
+  // Check that src is not NULL.
+  if (!src) {
+    snprintf(result.errmsg, sizeof(result.errmsg), "src is NULL");
+    goto bail;
+  }
+
+  // Check that src is NUL terminated.
+  if (src[len]) {
+    snprintf(result.errmsg, sizeof(result.errmsg),
+             "src[] must be NUL terminated");
+    goto bail;
+  }
+
+  // If user insists, check that src[] is a valid utf8 string.
+  if (toml_option.check_utf8) {
+    int line = 1; // keeps track of line number
+    for (int i = 0; i < len;) {
+      uint32_t ch;
+      int n = utf8_to_ucs(src + i, len - i, &ch);
+      if (n < 0) {
+        snprintf(result.errmsg, sizeof(result.errmsg),
+                 "invalid UTF8 char on line %d", line);
+        goto bail;
+      }
+      if (0xD800 <= ch && ch <= 0xDFFF) {
+        // explicitly prohibit surrogates (non-scalar unicode code point)
+        snprintf(result.errmsg, sizeof(result.errmsg),
+                 "invalid UTF8 char \\u%04x on line %d", ch, line);
+        goto bail;
+      }
+      line += (ch == '\n' ? 1 : 0);
+      i += n;
+    }
+  }
+
+  // Initialize parser
+  pp->toptab = mkdatum_at(TOML_TABLE, 1, 1);
+  pp->curtab = &pp->toptab;
+  pp->ebuf.ptr = result.errmsg; // parse error will be printed into pp->ebuf
+  pp->ebuf.len = sizeof(result.errmsg);
+
+  // Alloc memory pool (extra bytes for NUL term, safety, and the source name)
+  int namelen = name ? (int)strlen(name) + 1 : 0;
+  pp->pool = pool_create(len + 10 + namelen);
+  if (!pp->pool) {
+    snprintf(result.errmsg, sizeof(result.errmsg), "out of memory");
+    goto bail;
+  }
+  if (name) {
+    char *p = pool_alloc(pp->pool, namelen);
+    if (!p) {
+      snprintf(result.errmsg, sizeof(result.errmsg), "out of memory");
+      goto bail;
+    }
+    memcpy(p, name, namelen);
+    pp->srcname = p;
+  }
+
+  // Initialize scanner. Scan error will be printed into pp->ebuf.
+  scan_init(&pp->scanner, src, len, pp->ebuf.ptr, pp->ebuf.len);
+
+  // Keep parsing until FIN
+  for (;;) {
+    token_t tok;
+    if (scan_key(&pp->scanner, &tok)) {
+      goto bail;
+    }
+    // break on FIN
+    if (tok.toktyp == TOK_FIN) {
+      break;
+    }
+    switch (tok.toktyp) {
+    case TOK_ENDL: // skip blank lines
+      continue;
+    case TOK_LBRACK:
+      if (parse_std_table_expr(pp, tok)) {
+        goto bail;
+      }
+      break;
+    case TOK_LLBRACK:
+      if (parse_array_table_expr(pp, tok)) {
+        goto bail;
+      }
+      break;
+    default:
+      // non-blank line: parse an expression
+      if (parse_keyvalue_expr(pp, tok)) {
+        goto bail;
+      }
+      break;
+    }
+    // each expression must be followed by newline
+    if (scan_key(&pp->scanner, &tok)) {
+      goto bail;
+    }
+    if (tok.toktyp == TOK_FIN || tok.toktyp == TOK_ENDL) {
+      continue;
+    }
+    SETERROR(pp->ebuf, tok.lineno, "ENDL expected");
+    goto bail;
+  }
+
+  // return result
+  result.ok = true;
+  set_source_recursive(&pp->toptab, pp->srcname);
+  result.toptab = pp->toptab;
+  result.__internal = (void *)pp->pool;
+  return result;
+
+bail:
+  // return error
+  datum_free(&pp->toptab);
+  pool_destroy(pp->pool);
+  result.ok = false;
+  if (result.errmsg[0] == '\0') {
+    assert(0);
+    snprintf(result.errmsg, sizeof(result.errmsg), "Error near line %d\n",
+             pp->scanner.lineno);
+  }
+  return result;
+}
+
+// Convert a (LITSTRING, LIT, MLLITSTRING, MLSTRING, or STRING) token to a
+// datum.
+static int token_to_string(parser_t *pp, token_t tok, toml_datum_t *ret) {
+  *ret = mkdatum_at(TOML_STRING, tok.lineno, tok.colno);
+  span_t span;
+  DO(parse_norm(pp, tok, &span));
+  ret->u.str.ptr = (char *)span.ptr;
+  ret->u.str.len = span.len;
+  return 0;
+}
+
+// Convert a TIME/DATE/DATETIME/DATETIMETZ to a datum
+static int token_to_timestamp(parser_t *pp, token_t tok, toml_datum_t *ret) {
+  (void)pp;
+  static const toml_type_t map[] = {[TOK_TIME] = TOML_TIME,
+                                    [TOK_DATE] = TOML_DATE,
+                                    [TOK_DATETIME] = TOML_DATETIME,
+                                    [TOK_DATETIMETZ] = TOML_DATETIMETZ};
+  switch (tok.toktyp) {
+  case TOK_TIME:
+  case TOK_DATE:
+  case TOK_DATETIME:
+  case TOK_DATETIMETZ:
+    break;
+  default:
+    assert(0 && "unexpected token type");
+    return -1;
+  }
+
+  *ret = mkdatum_at(map[tok.toktyp], tok.lineno, tok.colno);
+  ret->u.ts.year = tok.u.tsval.year;
+  ret->u.ts.month = tok.u.tsval.month;
+  ret->u.ts.day = tok.u.tsval.day;
+  ret->u.ts.hour = tok.u.tsval.hour;
+  ret->u.ts.minute = tok.u.tsval.minute;
+  ret->u.ts.second = tok.u.tsval.sec;
+  ret->u.ts.usec = tok.u.tsval.usec;
+  ret->u.ts.tz = tok.u.tsval.tz;
+  return 0;
+}
+
+// Convert an int64 token to a datum.
+static int token_to_int64(parser_t *pp, token_t tok, toml_datum_t *ret) {
+  (void)pp;
+  assert(tok.toktyp == TOK_INTEGER);
+  *ret = mkdatum_at(TOML_INT64, tok.lineno, tok.colno);
+  ret->u.int64 = tok.u.int64;
+  return 0;
+}
+
+// Convert a fp64 token to a datum.
+static int token_to_fp64(parser_t *pp, token_t tok, toml_datum_t *ret) {
+  (void)pp;
+  assert(tok.toktyp == TOK_FLOAT);
+  *ret = mkdatum_at(TOML_FP64, tok.lineno, tok.colno);
+  ret->u.fp64 = tok.u.fp64;
+  return 0;
+}
+
+// Convert a boolean token to a datum.
+static int token_to_boolean(parser_t *pp, token_t tok, toml_datum_t *ret) {
+  (void)pp;
+  assert(tok.toktyp == TOK_BOOL);
+  *ret = mkdatum_at(TOML_BOOLEAN, tok.lineno, tok.colno);
+  ret->u.boolean = tok.u.b1;
+  return 0;
+}
+
+// Parse a multipart key. Return 0 on success, -1 otherwise.
+static int parse_key(parser_t *pp, token_t tok, keypart_t *ret_keypart) {
+  ret_keypart->nspan = 0;
+  // key = simple-key | dotted_key
+  // simple-key = STRING | LITSTRING | LIT
+  // dotted-key = simple-key (DOT simple-key)+
+  if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING &&
+      tok.toktyp != TOK_LIT) {
+    return SETERROR(pp->ebuf, tok.lineno, "missing key");
+  }
+
+  int n = 0;
+  span_t *kpspan = ret_keypart->span;
+
+  // Normalize the first keypart
+  if (parse_norm(pp, tok, &kpspan[n])) {
+    return SETERROR(pp->ebuf, tok.lineno,
+                    "unable to normalize string; probably a unicode issue");
+  }
+  n++;
+
+  // Scan and normalize the second to last keypart
+  while (1) {
+    scanner_state_t mark = scan_mark(&pp->scanner);
+
+    // Eat the dot if it is there
+    DO(scan_key(&pp->scanner, &tok));
+
+    // If not a dot, we are done with keyparts.
+    if (tok.toktyp != TOK_DOT) {
+      scan_restore(&pp->scanner, mark);
+      break;
+    }
+
+    // Scan the n-th key
+    DO(scan_key(&pp->scanner, &tok));
+
+    if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING &&
+        tok.toktyp != TOK_LIT) {
+      return SETERROR(pp->ebuf, tok.lineno, "expects a string in dotted-key");
+    }
+
+    if (n >= KEYPARTMAX) {
+      return SETERROR(pp->ebuf, tok.lineno, "too many key parts");
+    }
+
+    // Normalize the n-th key.
+    DO(parse_norm(pp, tok, &kpspan[n]));
+    n++;
+  }
+
+  // This key has n parts.
+  ret_keypart->nspan = n;
+  return 0;
+}
+
+// Starting at toptab, descend following keypart[]. If a key does not
+// exist in the current table, create a new table entry for the
+// key. Returns the final table represented by the key.
+static toml_datum_t *descend_keypart(parser_t *pp, int lineno, int colno,
+                                     toml_datum_t *toptab, keypart_t *keypart,
+                                     bool stdtabexpr) {
+  toml_datum_t *tab = toptab; // current tab
+
+  for (int i = 0; i < keypart->nspan; i++) {
+    const char *reason;
+    // Find the i-th keypart
+    int j = tab_find(tab, keypart->span[i]);
+    // Not found: add a new (key, tab) pair.
+    if (j < 0) {
+      toml_datum_t newtab = mkdatum_at(TOML_TABLE, lineno, colno);
+      newtab.flag |= stdtabexpr ? FLAG_STDEXPR : 0;
+      if (tab_add(tab, keypart->span[i], newtab, &reason)) {
+        SETERROR(pp->ebuf, lineno, "%s", reason);
+        return NULL;
+      }
+      tab = &tab->u.tab.value[tab->u.tab.size - 1]; // descend
+      continue;
+    }
+
+    // Found: extract the value of the key.
+    toml_datum_t *value = &tab->u.tab.value[j];
+
+    // If the value is a table, descend.
+    if (value->type == TOML_TABLE) {
+      tab = value; // descend
+      continue;
+    }
+
+    // If the value is an array: locate the last entry and descend.
+    if (value->type == TOML_ARRAY) {
+      // If empty: error.
+      if (value->u.arr.size <= 0) {
+        SETERROR(pp->ebuf, lineno, "array %s has no elements",
+                 keypart->span[i].ptr);
+        return NULL;
+      }
+
+      // Extract the last element of the array.
+      value = &value->u.arr.elem[value->u.arr.size - 1];
+
+      // It must be a table!
+      if (value->type != TOML_TABLE) {
+        SETERROR(pp->ebuf, lineno, "array %s must be array of tables",
+                 keypart->span[i].ptr);
+        return NULL;
+      }
+      tab = value; // descend
+      continue;
+    }
+
+    // key not found
+    SETERROR(pp->ebuf, lineno, "cannot locate table at key %s",
+             keypart->span[i].ptr);
+    return NULL;
+  }
+
+  // Return the table corresponding to the keypart[].
+  return tab;
+}
+
+// Recursively set flags on datum
+static void set_flag_recursive(toml_datum_t *datum, uint32_t flag) {
+  datum->flag |= flag;
+  switch (datum->type) {
+  case TOML_ARRAY:
+    for (int i = 0, top = datum->u.arr.size; i < top; i++) {
+      set_flag_recursive(&datum->u.arr.elem[i], flag);
+    }
+    break;
+  case TOML_TABLE:
+    for (int i = 0, top = datum->u.tab.size; i < top; i++) {
+      set_flag_recursive(&datum->u.tab.value[i], flag);
+    }
+    break;
+  default:
+    break;
+  }
+}
+
+// Parse an inline array.
+static int parse_inline_array(parser_t *pp, token_t tok,
+                              toml_datum_t *ret_datum) {
+  assert(tok.toktyp == TOK_LBRACK);
+  *ret_datum = mkdatum_at(TOML_ARRAY, tok.lineno, tok.colno);
+  int need_comma = 0;
+
+  // loop until RBRACK
+  for (;;) {
+    // skip ENDL
+    do {
+      DO(scan_value(&pp->scanner, &tok));
+    } while (tok.toktyp == TOK_ENDL);
+
+    // If got an RBRACK: done!
+    if (tok.toktyp == TOK_RBRACK) {
+      break;
+    }
+
+    // If got a COMMA: check if it is expected.
+    if (tok.toktyp == TOK_COMMA) {
+      if (need_comma) {
+        need_comma = 0;
+        continue;
+      }
+      return SETERROR(pp->ebuf, tok.lineno,
+                      "syntax error while parsing array: unexpected comma");
+    }
+
+    // Not a comma, but need a comma: error!
+    if (need_comma) {
+      return SETERROR(pp->ebuf, tok.lineno,
+                      "syntax error while parsing array: missing comma");
+    }
+
+    // This is a valid value! Obtain the value.
+    toml_datum_t value = DATUM_ZERO;
+    if (parse_val(pp, tok, &value)) {
+      datum_free(&value);
+      return -1;
+    }
+
+    // Add the value to the array.
+    const char *reason;
+    toml_datum_t *pelem = arr_emplace(ret_datum, &reason);
+    if (!pelem) {
+      datum_free(&value);
+      return SETERROR(pp->ebuf, tok.lineno, "while parsing array: %s", reason);
+    }
+    *pelem = value;
+
+    // Need comma before the next value.
+    need_comma = 1;
+  }
+
+  // Set the INLINE flag for all things in this array.
+  set_flag_recursive(ret_datum, FLAG_INLINED);
+  return 0;
+}
+
+// Parse an inline table.
+static int parse_inline_table(parser_t *pp, token_t tok,
+                              toml_datum_t *ret_datum) {
+  assert(tok.toktyp == TOK_LBRACE);
+  *ret_datum = mkdatum_at(TOML_TABLE, tok.lineno, tok.colno);
+  bool need_comma = 0;
+  bool was_comma = 0;
+
+  // loop until RBRACE
+  for (;;) {
+    DO(scan_key(&pp->scanner, &tok));
+
+    // Got an RBRACE: done!
+    if (tok.toktyp == TOK_RBRACE) {
+      if (was_comma) {
+        /*
+        return SETERROR(pp->ebuf, tok.lineno,
+                        "extra comma before closing brace");
+        */
+        // extra comma before RBRACE is allowed for v1.1
+        (void)0;
+      }
+      break;
+    }
+
+    // Got a comma: check if it is expected.
+    if (tok.toktyp == TOK_COMMA) {
+      if (need_comma) {
+        need_comma = 0, was_comma = 1;
+        continue;
+      }
+      return SETERROR(pp->ebuf, tok.lineno, "unexpected comma");
+    }
+
+    // Newline not allowed in inline table.
+    // newline is allowed in v1.1
+    if (tok.toktyp == TOK_ENDL) {
+      // return SETERROR(pp->ebuf, tok.lineno, "unexpected newline");
+      continue;
+    }
+
+    // Not a comma, but need a comma: error!
+    if (need_comma) {
+      return SETERROR(pp->ebuf, tok.lineno, "missing comma");
+    }
+
+    // Get the keyparts
+    keypart_t keypart = {0};
+    int keylineno = tok.lineno;
+    int keycolno = tok.colno;
+    DO(parse_key(pp, tok, &keypart));
+
+    // Descend to one keypart before last
+    assert(keypart.nspan > 0);
+    span_t lastkeypart = keypart.span[--keypart.nspan];
+    toml_datum_t *tab =
+        descend_keypart(pp, keylineno, keycolno, ret_datum, &keypart, false);
+    if (!tab) {
+      return -1;
+    }
+
+    // If tab is a previously declared inline table: error.
+    if (tab->flag & FLAG_INLINED) {
+      return SETERROR(pp->ebuf, tok.lineno, "inline table cannot be extended");
+    }
+
+    // We are explicitly defining it now.
+    tab->flag |= FLAG_EXPLICIT;
+
+    // match EQUAL
+    DO(scan_value(&pp->scanner, &tok));
+
+    if (tok.toktyp != TOK_EQUAL) {
+      if (tok.toktyp == TOK_ENDL) {
+        return SETERROR(pp->ebuf, tok.lineno, "unexpected newline");
+      } else {
+        return SETERROR(pp->ebuf, tok.lineno, "missing '='");
+      }
+    }
+
+    // obtain the value
+    DO(scan_value(&pp->scanner, &tok));
+    toml_datum_t value = DATUM_ZERO;
+    if (parse_val(pp, tok, &value)) {
+      datum_free(&value);
+      return -1;
+    }
+
+    // Add the value to tab.
+    const char *reason;
+    if (tab_add(tab, lastkeypart, value, &reason)) {
+      datum_free(&value);
+      return SETERROR(pp->ebuf, tok.lineno, "%s", reason);
+    }
+    need_comma = 1, was_comma = 0;
+  }
+
+  set_flag_recursive(ret_datum, FLAG_INLINED);
+  return 0;
+}
+
+// Parse a value.
+static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret) {
+  *ret = DATUM_ZERO; // initialize
+
+  // val = string / boolean / array / inline-table / date-time / float / integer
+  switch (tok.toktyp) {
+  case TOK_STRING:
+  case TOK_MLSTRING:
+  case TOK_LITSTRING:
+  case TOK_MLLITSTRING:
+    return token_to_string(pp, tok, ret);
+  case TOK_TIME:
+  case TOK_DATE:
+  case TOK_DATETIME:
+  case TOK_DATETIMETZ:
+    return token_to_timestamp(pp, tok, ret);
+  case TOK_INTEGER:
+    return token_to_int64(pp, tok, ret);
+  case TOK_FLOAT:
+    return token_to_fp64(pp, tok, ret);
+  case TOK_BOOL:
+    return token_to_boolean(pp, tok, ret);
+  case TOK_LBRACK: // inline-array
+    return parse_inline_array(pp, tok, ret);
+  case TOK_LBRACE: // inline-table
+    return parse_inline_table(pp, tok, ret);
+  default:
+    return SETERROR(pp->ebuf, tok.lineno, "missing value");
+  }
+}
+
+// Parse a standard table expression, and set the curtab of the parser
+// to the table referenced.  A standard table expression is a line
+// like [a.b.c.d].
+static int parse_std_table_expr(parser_t *pp, token_t tok) {
+  // std-table = [ key ]
+  // Eat the [
+  assert(tok.toktyp == TOK_LBRACK); // [ ate by caller
+
+  // Read the first keypart
+  DO(scan_key(&pp->scanner, &tok));
+
+  // Extract the keypart[]
+  int keylineno = tok.lineno;
+  int keycolno = tok.colno;
+  keypart_t keypart;
+  DO(parse_key(pp, tok, &keypart));
+
+  // Eat the ]
+  DO(scan_key(&pp->scanner, &tok));
+  if (tok.toktyp != TOK_RBRACK) {
+    return SETERROR(pp->ebuf, tok.lineno, "missing right-bracket");
+  }
+
+  // Descend to one keypart before last.
+  span_t lastkeypart = keypart.span[--keypart.nspan];
+
+  // Descend keypart from the toptab.
+  toml_datum_t *tab =
+      descend_keypart(pp, keylineno, keycolno, &pp->toptab, &keypart, true);
+  if (!tab) {
+    return -1;
+  }
+
+  // Look for the last keypart in the final tab
+  int j = tab_find(tab, lastkeypart);
+  if (j < 0) {
+    // If not found: add it.
+    if (tab->flag & FLAG_INLINED) {
+      return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended");
+    }
+    const char *reason;
+    toml_datum_t newtab = mkdatum_at(TOML_TABLE, keylineno, keycolno);
+    newtab.flag |= FLAG_STDEXPR;
+    if (tab_add(tab, lastkeypart, newtab, &reason)) {
+      return SETERROR(pp->ebuf, keylineno, "%s", reason);
+    }
+    // this is the new tab
+    tab = &tab->u.tab.value[tab->u.tab.size - 1];
+  } else {
+    // Found: check for errors
+    tab = &tab->u.tab.value[j];
+    if (tab->flag & FLAG_EXPLICIT) {
+      /*
+        This is not OK:
+        [x.y.z]
+        [x.y.z]
+
+        but this is OK:
+        [x.y.z]
+        [x]
+      */
+      return SETERROR(pp->ebuf, keylineno, "table defined more than once");
+    }
+    if (!(tab->flag & FLAG_STDEXPR)) {
+      /*
+      [t1]			# OK
+      t2.t3.v = 0		# OK
+      [t1.t2]   		# should FAIL  - t2 was non-explicit but was not
+      created by std-table-expr
+      */
+      return SETERROR(pp->ebuf, keylineno, "table defined before");
+    }
+  }
+
+  // Set explicit flag on tab
+  tab->flag |= FLAG_EXPLICIT;
+  tab->lineno = keylineno;
+  tab->colno = keycolno;
+
+  // Set tab as curtab of the parser
+  pp->curtab = tab;
+  return 0;
+}
+
+// Parse an array table expression, and set the curtab of the parser
+// to the table referenced. A standard array table expression is a line
+// like [[a.b.c.d]].
+static int parse_array_table_expr(parser_t *pp, token_t tok) {
+  // array-table = [[ key ]]
+  assert(tok.toktyp == TOK_LLBRACK); // [[ ate by caller
+
+  // Read the first keypart
+  DO(scan_key(&pp->scanner, &tok));
+
+  int keylineno = tok.lineno;
+  int keycolno = tok.colno;
+  keypart_t keypart;
+  DO(parse_key(pp, tok, &keypart));
+
+  // eat the ]]
+  token_t rrb;
+  DO(scan_key(&pp->scanner, &rrb));
+  if (rrb.toktyp != TOK_RRBRACK) {
+    return SETERROR(pp->ebuf, rrb.lineno, "missing ']]'");
+  }
+
+  // remove the last keypart from keypart[]
+  assert(keypart.nspan > 0);
+  span_t lastkeypart = keypart.span[--keypart.nspan];
+
+  // descend intermediate keys from toptab
+  toml_datum_t *tab =
+      descend_keypart(pp, keylineno, keycolno, &pp->toptab, &keypart, true);
+  if (!tab)
+    return -1;
+
+  // For the final keypart, make sure entry at key is an array of tables
+  const char *reason;
+  int idx = tab_find(tab, lastkeypart);
+  if (idx == -1) {
+    // If not found, add an array of table.
+    if (tab->flag & FLAG_INLINED) {
+      return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended");
+    }
+    toml_datum_t newarr = mkdatum_at(TOML_ARRAY, keylineno, keycolno);
+    if (tab_add(tab, lastkeypart, newarr, &reason)) {
+      return SETERROR(pp->ebuf, keylineno, "%s", reason);
+    }
+    idx = tab_find(tab, lastkeypart);
+    assert(idx >= 0);
+  }
+  // Check that this is an array.
+  if (tab->u.tab.value[idx].type != TOML_ARRAY) {
+    return SETERROR(pp->ebuf, keylineno, "entry must be an array");
+  }
+  // Add an empty table to the array
+  toml_datum_t *arr = &tab->u.tab.value[idx];
+  if (arr->flag & FLAG_INLINED) {
+    return SETERROR(pp->ebuf, keylineno, "cannot extend a static array");
+  }
+  toml_datum_t *pelem = arr_emplace(arr, &reason);
+  if (!pelem) {
+    return SETERROR(pp->ebuf, keylineno, "%s", reason);
+  }
+  *pelem = mkdatum_at(TOML_TABLE, keylineno, keycolno);
+
+  // Set the last element of this array as curtab of the parser
+  pp->curtab = &arr->u.arr.elem[arr->u.arr.size - 1];
+  assert(pp->curtab->type == TOML_TABLE);
+
+  return 0;
+}
+
+// Parse an expression. A toml doc is just a list of expressions.
+static int parse_keyvalue_expr(parser_t *pp, token_t tok) {
+  // Obtain the key
+  int keylineno = tok.lineno;
+  int keycolno = tok.colno;
+  keypart_t keypart;
+  DO(parse_key(pp, tok, &keypart));
+
+  // match the '='
+  DO(scan_key(&pp->scanner, &tok));
+  if (tok.toktyp != TOK_EQUAL) {
+    return SETERROR(pp->ebuf, tok.lineno, "expect '='");
+  }
+
+  // Locate the last table using keypart[]
+  const char *reason;
+  toml_datum_t *tab = pp->curtab;
+  for (int i = 0; i < keypart.nspan - 1; i++) {
+    int j = tab_find(tab, keypart.span[i]);
+    if (j < 0) {
+      if (i > 0 && (tab->flag & FLAG_EXPLICIT)) {
+        return SETERROR(
+            pp->ebuf, keylineno,
+            "cannot extend a previously defined table using dotted expression");
+      }
+      toml_datum_t newtab = mkdatum_at(TOML_TABLE, keylineno, keycolno);
+      if (tab_add(tab, keypart.span[i], newtab, &reason)) {
+        return SETERROR(pp->ebuf, keylineno, "%s", reason);
+      }
+      tab = &tab->u.tab.value[tab->u.tab.size - 1];
+      continue;
+    }
+    toml_datum_t *value = &tab->u.tab.value[j];
+    if (value->type == TOML_TABLE) {
+      tab = value;
+      continue;
+    }
+    if (value->type == TOML_ARRAY) {
+      return SETERROR(pp->ebuf, keylineno,
+                      "encountered previously declared array '%s'",
+                      keypart.span[i].ptr);
+    }
+    return SETERROR(pp->ebuf, keylineno, "cannot locate table at '%s'",
+                    keypart.span[i].ptr);
+  }
+
+  // Check for disallowed situations.
+  if (tab->flag & FLAG_INLINED) {
+    return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended");
+  }
+  if (keypart.nspan > 1 && (tab->flag & FLAG_EXPLICIT)) {
+    return SETERROR(
+        pp->ebuf, keylineno,
+        "cannot extend a previously defined table using dotted expression");
+  }
+
+  // Obtain the value
+  DO(scan_value(&pp->scanner, &tok));
+  toml_datum_t newval = DATUM_ZERO;
+  if (parse_val(pp, tok, &newval)) {
+    datum_free(&newval);
+    return -1;
+  }
+
+  // Add a new key/value for tab.
+  if (tab_add(tab, keypart.span[keypart.nspan - 1], newval, &reason)) {
+    datum_free(&newval);
+    return SETERROR(pp->ebuf, keylineno, "%s", reason);
+  }
+
+  return 0;
+}
+
+// Normalize a LIT/STRING/MLSTRING/LITSTRING/MLLITSTRING
+// -> unescape all escaped chars
+// The returned string is allocated out of pp->pool
+static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span) {
+  // Allocate a buffer to store the normalized string. Add one
+  // extra-byte for terminating NUL.
+  char *p = pool_alloc(pp->pool, tok.str.len + 1);
+  if (!p) {
+    return SETERROR(pp->ebuf, tok.lineno, "out of memory");
+  }
+
+  // Copy from token string into buffer
+  memcpy(p, tok.str.ptr, tok.str.len);
+  p[tok.str.len] = 0; // additional NUL term for safety
+
+  ret_span->ptr = p;
+  ret_span->len = tok.str.len;
+
+  switch (tok.toktyp) {
+  case TOK_LIT:
+  case TOK_LITSTRING:
+  case TOK_MLLITSTRING:
+    // no need to handle escape chars
+    return 0;
+
+  case TOK_STRING:
+  case TOK_MLSTRING:
+    // need to handle escape chars
+    break;
+
+  default:
+    return SETERROR(pp->ebuf, 0, "internal: arg must be a string");
+  }
+
+  // if there is no escape char, then done!
+  if (!tok.u.escp) {
+    return 0; // success
+  }
+
+  // p points to the backslash
+  p += (tok.u.escp - tok.str.ptr);
+  assert(p - ret_span->ptr == tok.u.escp - tok.str.ptr);
+  assert(*p == '\\');
+
+  // Normalize the escaped chars
+  char *dst = p;
+  while (*p) {
+    if (*p != '\\') {
+      *dst++ = *p++;
+      continue;
+    }
+    switch (p[1]) {
+    case '"':
+    case '\\':
+      *dst++ = p[1];
+      p += 2;
+      continue;
+    case 'b':
+      *dst++ = '\b';
+      p += 2;
+      continue;
+    case 't':
+      *dst++ = '\t';
+      p += 2;
+      continue;
+    case 'n':
+      *dst++ = '\n';
+      p += 2;
+      continue;
+    case 'f':
+      *dst++ = '\f';
+      p += 2;
+      continue;
+    case 'r':
+      *dst++ = '\r';
+      p += 2;
+      continue;
+    case 'e':
+      *dst++ = '\033';
+      p += 2;
+      continue;
+    case 'x': {
+      char buf[3];
+      memcpy(buf, p + 2, 2);
+      buf[2] = 0;
+      // There is no need to check for two hex digits here because
+      // the scanner already checked it.
+      int32_t ucs = strtol(buf, 0, 16);
+      int n = ucs_to_utf8(ucs, dst);
+      if (n < 0) {
+        return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8",
+                        buf);
+      }
+      dst += n;
+      p += 2 + 2; // \xNN
+      continue;
+    }
+    case 'u':
+    case 'U': {
+      char buf[9];
+      int sz = (p[1] == 'u' ? 4 : 8);
+      memcpy(buf, p + 2, sz);
+      buf[sz] = 0;
+      // There is no need to check for 4 or 8 hex digits here because
+      // the scanner already checked it.
+      int32_t ucs = strtol(buf, 0, 16);
+      if (0xD800 <= ucs && ucs <= 0xDFFF) {
+        // explicitly prohibit surrogates (non-scalar unicode code point)
+        return SETERROR(pp->ebuf, tok.lineno, "invalid UTF8 char \\u%04x", ucs);
+      }
+      int n = ucs_to_utf8(ucs, dst);
+      if (n < 0) {
+        return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8",
+                        buf);
+      }
+      dst += n;
+      p += 2 + sz; // \uNNNN or \UNNNNNNNN
+      continue;
+    }
+
+    case ' ':
+    case '\t':
+    case '\r':
+      // line-ending backslash
+      // --- allow for extra whitespace chars after backslash
+      // --- skip until newline
+      p++;                     // skip the escape char
+      p += strspn(p, " \t\r"); // skip whitespaces
+      if (*p != '\n') {
+        return SETERROR(pp->ebuf, tok.lineno,
+                        "unexpected char after line-ending backslash");
+      }
+      // fallthru
+    case '\n':
+      // skip all whitespaces including newline
+      p++;
+      p += strspn(p, " \t\r\n");
+      continue;
+    default:
+      return SETERROR(pp->ebuf, tok.lineno,
+                      "internal: unknown escape char \\%c", p[1]);
+    }
+  }
+  *dst = 0;
+  ret_span->len = dst - ret_span->ptr;
+  return 0;
+}
+
+// ===================================================================
+// ==    SCANNER SECTION
+// ===================================================================
+
+// Get the next char
+static int scan_get(scanner_t *sp) {
+  int ret = TOK_FIN;
+  const char *p = sp->cur;
+  if (p < sp->endp) {
+    ret = *p++;
+    if (ret == '\r' && p < sp->endp && *p == '\n') {
+      ret = *p++;
+    }
+  }
+  sp->cur = p;
+  if (ret == '\n') {
+    sp->lineno += 1;
+    sp->line_start = p;
+  }
+  return ret;
+}
+
+// Check if the next char matches ch.
+static inline bool scan_match(scanner_t *sp, int ch) {
+  const char *p = sp->cur;
+  // exact match? done.
+  if (p < sp->endp && *p == ch) {
+    return true;
+  }
+  // \n also matches \r\n
+  if (ch == '\n' && p + 1 < sp->endp) {
+    return p[0] == '\r' && p[1] == '\n';
+  }
+  // not a match
+  return false;
+}
+
+// Check if the next char is in accept[].
+static bool scan_matchany(scanner_t *sp, const char *accept) {
+  for (; *accept; accept++) {
+    if (scan_match(sp, *accept)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+// Check if the next n chars match ch.
+static inline bool scan_nmatch(scanner_t *sp, int ch, int n) {
+  assert(ch != '\n'); // not handled
+  if (sp->cur + n > sp->endp) {
+    return false;
+  }
+  const char *p = sp->cur;
+  int i;
+  for (i = 0; i < n && p[i] == ch; i++)
+    ;
+  return i == n;
+}
+
+// Initialize a token.
+static inline token_t mktoken(scanner_t *sp, toktyp_t typ) {
+  token_t tok = {0};
+  tok.toktyp = typ;
+  tok.str.ptr = sp->cur;
+  tok.lineno = sp->lineno;
+  tok.colno = (int)(sp->cur - sp->line_start) + 1;
+  return tok;
+}
+
+#define S_GET() scan_get(sp)
+#define S_MATCH(ch) scan_match(sp, (ch))
+#define S_MATCH3(ch) scan_nmatch(sp, (ch), 3)
+#define S_MATCH4(ch) scan_nmatch(sp, (ch), 4)
+#define S_MATCH6(ch) scan_nmatch(sp, (ch), 6)
+
+static inline bool is_valid_char(int ch) {
+  // i.e. (0x20 <= ch && ch <= 0x7e) || (ch & 0x80);
+  return isprint((unsigned char)ch) || (ch & 0x80);
+}
+
+static inline bool is_hex_char(int ch) {
+  ch = toupper((unsigned char)ch);
+  return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F');
+}
+
+// Initialize a scanner
+static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf,
+                      int errbufsz) {
+  memset(sp, 0, sizeof(*sp));
+  sp->src = src;
+  sp->endp = src + len;
+  assert(*sp->endp == '\0');
+  sp->cur = src;
+  sp->lineno = 1;
+  sp->line_start = src;
+  sp->ebuf.ptr = errbuf;
+  sp->ebuf.len = errbufsz;
+}
+
+// Scan escape sequence characters after a backslash.
+// Return #char match on success, 0 if no match, -1 on error.
+static int scan_escape_chars(scanner_t *sp) {
+  const char *p = sp->cur;
+  if (p >= sp->endp) {
+    return 0;
+  }
+  int ch = *p++;
+  if (ch && strchr("btnfre\"\\", ch)) {
+    return p - sp->cur;
+  }
+  int hex = (ch == 'x') ? 2 : (ch == 'u') ? 4 : (ch == 'U') ? 8 : 0;
+  if (hex) {
+    int i = 0;
+    for (; i < hex && p < sp->endp && is_hex_char(*p); i++, p++)
+      ;
+    if (i != hex) {
+      return SETERROR(sp->ebuf, sp->lineno, "expect %d hex digits after \\%c",
+                      hex, ch);
+    }
+    return p - sp->cur;
+  }
+  return 0;
+}
+
+// Scan """ ... """
+static int scan_multiline_string(scanner_t *sp, token_t *tok) {
+  assert(S_MATCH3('"'));
+  S_GET(), S_GET(), S_GET(); // skip opening """
+
+  // According to spec: trim first newline after """
+  if (S_MATCH('\n')) {
+    S_GET();
+  }
+
+  *tok = mktoken(sp, TOK_MLSTRING);
+  // scan until terminating """
+  const char *escp = NULL;
+  while (1) {
+    if (S_MATCH3('"')) {
+      if (S_MATCH4('"')) {
+        // special case... """abcd """" -> (abcd ")
+        // but sequences of 3 or more double quotes are not allowed
+        if (S_MATCH6('"')) {
+          return SETERROR(sp->ebuf, sp->lineno,
+                          "detected sequences of 3 or more double quotes");
+        } else {
+          ; // no problem
+        }
+      } else {
+        break; // found terminating """
+      }
+    }
+    int ch = S_GET();
+    if (ch == TOK_FIN) {
+      return SETERROR(sp->ebuf, sp->lineno, "unterminated \"\"\"");
+    }
+    // If non-escaped char ...
+    if (ch != '\\') {
+      if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) {
+        return SETERROR(sp->ebuf, sp->lineno, "invalid char in string");
+      }
+      continue;
+    }
+    // ch is backslash
+    if (!escp) {
+      escp = sp->cur - 1;	/* mark the first esc position */
+      assert(*escp == '\\');
+    }
+
+    // handle escape char
+    int cnt = scan_escape_chars(sp);
+    if (cnt < 0) {
+      return -1;
+    }
+    if (cnt > 0) {
+      sp->cur += cnt; // skip the escape sequence
+      continue;
+    }
+    assert(cnt == 0);
+
+    // handle line-ending backslash
+    ch = S_GET();
+    if (ch == ' ' || ch == '\t') {
+      // Although the spec does not allow for whitespace following a
+      // line-ending backslash, some standard tests expect it.
+      // Skip whitespace till EOL.
+      while (ch != TOK_FIN && ch && strchr(" \t", ch)) {
+        ch = S_GET();
+      }
+      if (ch != '\n') {
+        // Got a backslash followed by whitespace, followed by some char
+        // before newline
+        return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string");
+      }
+      // fallthru
+    }
+    if (ch == '\n') {
+      // got a line-ending backslash
+      // - skip all whitespaces
+      while (scan_matchany(sp, " \t\n")) {
+        S_GET();
+      }
+      continue;
+    }
+    return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string");
+  }
+  tok->str.len = sp->cur - tok->str.ptr;
+  tok->u.escp = escp;
+
+  assert(S_MATCH3('"'));
+  S_GET(), S_GET(), S_GET();
+  return 0;
+}
+
+// Scan " ... "
+static int scan_string(scanner_t *sp, token_t *tok) {
+  assert(S_MATCH('"'));
+  if (S_MATCH3('"')) {
+    return scan_multiline_string(sp, tok);
+  }
+  S_GET(); // skip opening "
+
+  // scan until closing "
+  *tok = mktoken(sp, TOK_STRING);
+  const char *escp = NULL;
+  while (!S_MATCH('"')) {
+    int ch = S_GET();
+    if (ch == TOK_FIN) {
+      return SETERROR(sp->ebuf, sp->lineno, "unterminated string");
+    }
+    // If non-escaped char ...
+    if (ch != '\\') {
+      if (!(is_valid_char(ch) || ch == ' ' || ch == '\t')) {
+        return SETERROR(sp->ebuf, sp->lineno, "invalid char in string");
+      }
+      continue;
+    }
+    // ch is backslash
+    if (!escp) {
+      escp = sp->cur - 1;
+      assert(*escp == '\\');
+    }
+
+    // handle escape char
+    int cnt = scan_escape_chars(sp);
+    if (cnt < 0) {
+      return -1;
+    }
+    if (cnt > 0) {
+      sp->cur += cnt;
+      continue;
+    }
+    return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string");
+  }
+  tok->str.len = sp->cur - tok->str.ptr;
+  tok->u.escp = escp;
+
+  assert(S_MATCH('"'));
+  S_GET(); // skip the terminating "
+  return 0;
+}
+
+// Scan ''' ... '''
+static int scan_multiline_litstring(scanner_t *sp, token_t *tok) {
+  assert(S_MATCH3('\''));
+  S_GET(), S_GET(), S_GET(); // skip opening '''
+
+  // According to spec: trim first newline after '''
+  if (S_MATCH('\n')) {
+    S_GET();
+  }
+
+  // scan until terminating '''
+  *tok = mktoken(sp, TOK_MLLITSTRING);
+  while (1) {
+    if (S_MATCH3('\'')) {
+      if (S_MATCH4('\'')) {
+        // special case... '''abcd '''' -> (abcd ')
+        // but sequences of 3 or more single quotes are not allowed
+        if (S_MATCH6('\'')) {
+          return SETERROR(sp->ebuf, sp->lineno,
+                          "sequences of 3 or more single quotes");
+        } else {
+          ; // no problem
+        }
+      } else {
+        break; // found terminating '''
+      }
+    }
+    int ch = S_GET();
+    if (ch == TOK_FIN) {
+      return SETERROR(sp->ebuf, sp->lineno,
+                      "unterminated multiline lit string");
+    }
+    if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) {
+      return SETERROR(sp->ebuf, sp->lineno, "invalid char in string");
+    }
+  }
+  tok->str.len = sp->cur - tok->str.ptr;
+
+  assert(S_MATCH3('\''));
+  S_GET(), S_GET(), S_GET();
+  return 0;
+}
+
+// Scan ' ... '
+static int scan_litstring(scanner_t *sp, token_t *tok) {
+  assert(S_MATCH('\''));
+  if (S_MATCH3('\'')) {
+    return scan_multiline_litstring(sp, tok);
+  }
+  S_GET(); // skip opening '
+
+  // scan until closing '
+  *tok = mktoken(sp, TOK_LITSTRING);
+  while (!S_MATCH('\'')) {
+    int ch = S_GET();
+    if (ch == TOK_FIN) {
+      return SETERROR(sp->ebuf, sp->lineno, "unterminated string");
+    }
+    if (!(is_valid_char(ch) || ch == '\t')) {
+      return SETERROR(sp->ebuf, sp->lineno, "invalid char in string");
+    }
+  }
+  tok->str.len = sp->cur - tok->str.ptr;
+  assert(S_MATCH('\''));
+  S_GET();
+  return 0;
+}
+
+static bool is_valid_date(int year, int month, int day) {
+  if (!(1 <= year)) {
+    return false;
+  }
+  if (!(1 <= month && month <= 12)) {
+    return false;
+  }
+  int is_leap_year = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
+  int days_in_month[] = {
+      31, 28 + is_leap_year, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+  return (1 <= day && day <= days_in_month[month - 1]);
+}
+
+static bool is_valid_time(int hour, int minute, int sec, int usec) {
+  if (!(0 <= hour && hour <= 23)) {
+    return false;
+  }
+  if (!(0 <= minute && minute <= 59)) {
+    return false;
+  }
+  if (!(0 <= sec && sec <= 59)) {
+    return false;
+  }
+  if (!(0 <= usec)) {
+    return false;
+  }
+  return true;
+}
+
+static bool is_valid_timezone(int minute) {
+  minute = (minute < 0 ? -minute : minute);
+  int hour = minute / 60;
+  minute = minute % 60;
+  if (!(0 <= hour && hour <= 23)) {
+    return false;
+  }
+  if (!(0 <= minute && minute < 60)) {
+    return false;
+  }
+  return true;
+}
+
+// Read an int (without signs) from the string p.
+static int read_int(const char *p, int *ret) {
+  const char *pp = p;
+  int64_t val = 0;
+  for (; isdigit((unsigned char)*p); p++) {
+    val = val * 10 + (*p - '0');
+    if (val > INT_MAX) {
+      return 0; // overflowed
+    }
+  }
+  *ret = (int)val;
+  return p - pp;
+}
+
+// Read a date as YYYY-MM-DD from p[]. Return #bytes consumed.
+static int read_date(const char *p, int *year, int *month, int *day) {
+  const char *pp = p;
+  int n;
+  n = read_int(p, year);
+  if (n != 4 || p[4] != '-') {
+    return 0;
+  }
+  n = read_int(p += n + 1, month);
+  if (n != 2 || p[2] != '-') {
+    return 0;
+  }
+  n = read_int(p += n + 1, day);
+  if (n != 2) {
+    return 0;
+  }
+  p += 2;
+  assert(p - pp == 10);
+  return p - pp;
+}
+
+// Read a time as HH:MM:SS.subsec from p[]. Return #bytes consumed.
+static int read_time(const char *p, int *hour, int *minute, int *second,
+                     int *usec) {
+  const char *pp = p;
+  int n;
+  *hour = *minute = *second = *usec = 0;
+  // scan hours
+  n = read_int(p, hour);
+  if (n != 2 || p[2] != ':') {
+    return 0;
+  }
+  p += 3;
+
+  // scan minutes
+  n = read_int(p, minute);
+  if (n != 2) {
+    return 0;
+  }
+  if (p[2] != ':') {
+    // seconds are optional in v1.1
+    p += 2;
+    return p - pp;
+  }
+  p += 3;
+
+  // scan seconds
+  n = read_int(p, second);
+  if (n != 2) {
+    return 0;
+  }
+  p += 2;
+
+  if (*p != '.') {
+    return p - pp;
+  }
+  p++; // skip the period
+  if (!isdigit((unsigned char)*p)) {
+    // trailing period
+    return 0;
+  }
+  int micro_factor = 100000;
+  while (isdigit((unsigned char)*p) && micro_factor) {
+    *usec += (*p - '0') * micro_factor;
+    micro_factor /= 10;
+    p++;
+  }
+  while (isdigit((unsigned char)*p))
+    p++; // consume extra sub-microsecond digits
+  return p - pp;
+}
+
+// Reads a timezone from p[]. Return #bytes consumed.
+// tzhours and tzminutes restricted to 2-char integers only.
+static int read_tzone(const char *p, char *tzsign, int *tzhour, int *tzminute) {
+  const char *pp = p;
+
+  // Default values
+  *tzhour = *tzminute = 0;
+  *tzsign = '+';
+
+  // Look for Zulu
+  if (*p == 'Z' || *p == 'z') {
+    return 1; // done! tz is +00:00.
+  }
+
+  // Look for +/-
+  *tzsign = *p++;
+  if (!(*tzsign == '+' || *tzsign == '-')) {
+    return 0;
+  }
+
+  // Look for HH:MM
+  int n;
+  n = read_int(p, tzhour);
+  if (n != 2 || p[2] != ':') {
+    return 0;
+  }
+  n = read_int(p += 3, tzminute);
+  if (n != 2) {
+    return 0;
+  }
+  p += 2;
+  return p - pp;
+}
+
+// Scan hh:mm:ss.xxxxx
+static int scan_time(scanner_t *sp, token_t *tok) {
+  int lineno = sp->lineno;
+  char buffer[20];
+  scan_copystr(sp, buffer, sizeof(buffer));
+
+  char *p = buffer;
+  int hour, minute, sec, usec;
+  int len = read_time(p, &hour, &minute, &sec, &usec);
+  if (len == 0) {
+    return SETERROR(sp->ebuf, lineno, "invalid time");
+  }
+  if (!is_valid_time(hour, minute, sec, usec)) {
+    return SETERROR(sp->ebuf, lineno, "invalid time");
+  }
+
+  *tok = mktoken(sp, TOK_TIME);
+  tok->str.len = len;
+  sp->cur += len;
+  tok->u.tsval.year = -1;
+  tok->u.tsval.month = -1;
+  tok->u.tsval.day = -1;
+  tok->u.tsval.hour = hour;
+  tok->u.tsval.minute = minute;
+  tok->u.tsval.sec = sec;
+  tok->u.tsval.usec = usec;
+  tok->u.tsval.tz = -1;
+  return 0;
+}
+
+// Scan a time, a date, a datetime, or a datatimetz
+static int scan_timestamp(scanner_t *sp, token_t *tok) {
+  int year, month, day, hour, minute, sec, usec, tz;
+  year = month = day = hour = minute = sec = usec = tz = -1;
+
+  int n;
+  // make a copy of sp->cur into buffer to ensure NUL terminated string
+  char buffer[80];
+  scan_copystr(sp, buffer, sizeof(buffer));
+
+  toktyp_t toktyp = TOK_FIN;
+  int lineno = sp->lineno;
+
+  // See if this a TIME only
+  const char *p = buffer;
+  if (isdigit((unsigned char)p[0]) && isdigit((unsigned char)p[1]) &&
+      p[2] == ':') {
+    n = read_time(buffer, &hour, &minute, &sec, &usec);
+    if (!n) {
+      return SETERROR(sp->ebuf, lineno, "invalid time");
+    }
+    toktyp = TOK_TIME;
+    p += n;
+    goto done;
+  }
+
+  // Try reading a DATE
+  n = read_date(p, &year, &month, &day);
+  if (!n) {
+    return SETERROR(sp->ebuf, lineno, "invalid date");
+  }
+  toktyp = TOK_DATE;
+  p += n;
+
+  // Check if there is no time component in addition
+  if (!((p[0] == 'T' || p[0] == ' ' || p[0] == 't') &&
+        isdigit((unsigned char)p[1]) && isdigit((unsigned char)p[2]) &&
+        p[3] == ':')) {
+    goto done; // no TIME component. we are done.
+  }
+
+  // Read the TIME
+  n = read_time(p += 1, &hour, &minute, &sec, &usec);
+  if (!n) {
+    return SETERROR(sp->ebuf, lineno, "invalid timestamp");
+  }
+  toktyp = TOK_DATETIME;
+  p += n;
+
+  // Read the (optional) timezone
+  char tzsign;
+  int tzhour, tzminute;
+  n = read_tzone(p, &tzsign, &tzhour, &tzminute);
+  if (n == 0) {
+    goto done; // datetime only
+  }
+  toktyp = TOK_DATETIMETZ;
+  p += n;
+
+  // Check tzminute range. This must be done here instead of is_valid_timezone()
+  // because we combine tzhour and tzminute into tz (by minutes only).
+  if (!(0 <= tzminute && tzminute < 60)) {
+    return SETERROR(sp->ebuf, lineno, "invalid timezone");
+  }
+  tz = (tzhour * 60 + tzminute) * (tzsign == '-' ? -1 : 1);
+  goto done; // datetimetz
+
+done:
+  *tok = mktoken(sp, toktyp);
+  n = p - buffer;
+  tok->str.len = n;
+  sp->cur += n;
+
+  tok->u.tsval.year = year;
+  tok->u.tsval.month = month;
+  tok->u.tsval.day = day;
+  tok->u.tsval.hour = hour;
+  tok->u.tsval.minute = minute;
+  tok->u.tsval.sec = sec;
+  tok->u.tsval.usec = usec;
+  tok->u.tsval.tz = tz;
+
+  // Do some error checks based on type
+  switch (tok->toktyp) {
+  case TOK_TIME:
+    if (!is_valid_time(hour, minute, sec, usec)) {
+      return SETERROR(sp->ebuf, lineno, "invalid time");
+    }
+    break;
+  case TOK_DATE:
+    if (!is_valid_date(year, month, day)) {
+      return SETERROR(sp->ebuf, lineno, "invalid date");
+    }
+    break;
+  case TOK_DATETIME:
+  case TOK_DATETIMETZ:
+    if (!is_valid_date(year, month, day)) {
+      return SETERROR(sp->ebuf, lineno, "invalid date");
+    }
+    if (!is_valid_time(hour, minute, sec, usec)) {
+      return SETERROR(sp->ebuf, lineno, "invalid time");
+    }
+    if (tok->toktyp == TOK_DATETIMETZ && !is_valid_timezone(tz)) {
+      return SETERROR(sp->ebuf, lineno, "invalid timezone");
+    }
+    break;
+  default:
+    assert(0);
+    return SETERROR(sp->ebuf, lineno, "internal error");
+  }
+
+  return 0;
+}
+
+// Given a toml number (int and float) in buffer[]:
+//   1. squeeze out '_'
+//   2. check for syntax restrictions
+static int process_numstr(char *buffer, int base, const char **reason) {
+  // squeeze out _
+  char *q = strchr(buffer, '_');
+  if (q) {
+    for (int i = q - buffer; buffer[i]; i++) {
+      if (buffer[i] != '_') {
+        *q++ = buffer[i];
+        continue;
+      }
+      int left = (i == 0) ? 0 : (unsigned char)buffer[i - 1];
+      int right = (unsigned char)buffer[i + 1];
+      if (!isdigit(left) && !(base == 16 && is_hex_char(left))) {
+        *reason = "underscore only allowed between digits";
+        return -1;
+      }
+      if (!isdigit(right) && !(base == 16 && is_hex_char(right))) {
+        *reason = "underscore only allowed between digits";
+        return -1;
+      }
+    }
+    *q = 0;
+  }
+
+  // decimal points must be surrounded by digits. Also, convert to lowercase.
+  for (int i = 0; buffer[i]; i++) {
+    if (buffer[i] == '.') {
+      if (i == 0 || !isdigit((unsigned char)buffer[i - 1]) ||
+          !isdigit((unsigned char)buffer[i + 1])) {
+        *reason = "decimal point must be surrounded by digits";
+        return -1;
+      }
+    } else if ('A' <= buffer[i] && buffer[i] <= 'Z') {
+      buffer[i] = tolower((unsigned char)buffer[i]);
+    }
+  }
+
+  if (base == 10) {
+    // check for leading 0:  '+01' is an error!
+    q = buffer;
+    q += (*q == '+' || *q == '-') ? 1 : 0;
+    if (q[0] == '0' && isdigit((unsigned char)q[1])) {
+      *reason = "leading 0 in numbers";
+      return -1;
+    }
+  }
+
+  return 0;
+}
+
+static int scan_float(scanner_t *sp, token_t *tok) {
+  char buffer[50]; // need to accommodate "9_007_199_254_740_991.0"
+  scan_copystr(sp, buffer, sizeof(buffer));
+
+  int lineno = sp->lineno;
+  char *p = buffer;
+  p += (*p == '+' || *p == '-') ? 1 : 0;
+  if (0 == memcmp(p, "nan", 3) || (0 == memcmp(p, "inf", 3))) {
+    p += 3;
+  } else {
+    p += strspn(p, "_0123456789eE.+-");
+  }
+  int len = p - buffer;
+  buffer[len] = 0;
+
+  const char *reason;
+  if (process_numstr(buffer, 10, &reason)) {
+    return SETERROR(sp->ebuf, lineno, "%s", reason);
+  }
+
+  errno = 0;
+  char *q;
+  double fp64 = strtod(buffer, &q);
+  if (errno || *q || q == buffer) {
+    return SETERROR(sp->ebuf, lineno, "error parsing float");
+  }
+
+  *tok = mktoken(sp, TOK_FLOAT);
+  tok->u.fp64 = fp64;
+  tok->str.len = len;
+  sp->cur += len;
+  return 0;
+}
+
+static int scan_number(scanner_t *sp, token_t *tok) {
+  const char *reason;
+  char buffer[50]; // need to accommodate "9_007_199_254_740_991.0"
+  scan_copystr(sp, buffer, sizeof(buffer));
+
+  char *p = buffer;
+  int lineno = sp->lineno;
+  // process %0x, %0o or %0b integers
+  if (p[0] == '0') {
+    const char *span = 0;
+    int base = 0;
+    switch (p[1]) {
+    case 'x':
+      base = 16;
+      span = "_0123456789abcdefABCDEF";
+      break;
+    case 'o':
+      base = 8;
+      span = "_01234567";
+      break;
+    case 'b':
+      base = 2;
+      span = "_01";
+      break;
+    }
+    if (base) {
+      p += 2;
+      p += strspn(p, span);
+      int len = p - buffer;
+      buffer[len] = 0;
+
+      if (process_numstr(buffer + 2, base, &reason)) {
+        return SETERROR(sp->ebuf, lineno, "%s", reason);
+      }
+
+      // use strtoll to obtain the value
+      *tok = mktoken(sp, TOK_INTEGER);
+      char *q;
+      errno = 0;
+      tok->u.int64 = strtoll(buffer + 2, &q, base);
+      if (errno || *q || q == buffer + 2) {
+        return SETERROR(sp->ebuf, lineno, "error parsing integer");
+      }
+      tok->str.len = len;
+      sp->cur += len;
+      return 0;
+    }
+  }
+
+  // handle inf/nan
+  if (*p == '+' || *p == '-') {
+    p++;
+  }
+  if (*p == 'i' || *p == 'n') {
+    return scan_float(sp, tok);
+  }
+
+  // regular int or float
+  p = buffer;
+  p += strspn(p, "0123456789_+-.eE");
+  int len = p - buffer;
+  buffer[len] = 0;
+
+  if (process_numstr(buffer, 10, &reason)) {
+    return SETERROR(sp->ebuf, lineno, "%s", reason);
+  }
+
+  *tok = mktoken(sp, TOK_INTEGER);
+  char *q;
+  errno = 0;
+  tok->u.int64 = strtoll(buffer, &q, 10);
+  if (errno || *q || q == buffer) {
+    if (*q && strchr(".eE", *q)) {
+      return scan_float(sp, tok); // try to fit a float
+    }
+    return SETERROR(sp->ebuf, lineno, "error parsing integer");
+  }
+
+  tok->str.len = len;
+  sp->cur += len;
+  return 0;
+}
+
+static int scan_bool(scanner_t *sp, token_t *tok) {
+  char buffer[10];
+  scan_copystr(sp, buffer, sizeof(buffer));
+
+  int lineno = sp->lineno;
+  bool val = false;
+  const char *p = buffer;
+  if (0 == strncmp(p, "true", 4)) {
+    val = true;
+    p += 4;
+  } else if (0 == strncmp(p, "false", 5)) {
+    val = false;
+    p += 5;
+  } else {
+    return SETERROR(sp->ebuf, lineno, "invalid boolean value");
+  }
+  if (*p && !strchr("# \r\n\t,}]", *p)) {
+    return SETERROR(sp->ebuf, lineno, "invalid boolean value");
+  }
+
+  int len = p - buffer;
+  *tok = mktoken(sp, TOK_BOOL);
+  tok->u.b1 = val;
+  tok->str.len = len;
+  sp->cur += len;
+  return 0;
+}
+
+// Check if the next token may be TIME
+static inline bool test_time(const char *p, const char *endp) {
+  return &p[2] < endp && isdigit((unsigned char)p[0]) &&
+         isdigit((unsigned char)p[1]) && p[2] == ':';
+}
+
+// Check if the next token may be DATE
+static inline bool test_date(const char *p, const char *endp) {
+  return &p[4] < endp && isdigit((unsigned char)p[0]) &&
+         isdigit((unsigned char)p[1]) && isdigit((unsigned char)p[2]) &&
+         isdigit((unsigned char)p[3]) && p[4] == '-';
+}
+
+// Check if the next token may be BOOL
+static inline bool test_bool(const char *p, const char *endp) {
+  return &p[0] < endp && (*p == 't' || *p == 'f');
+}
+
+// Check if the next token may be NUMBER
+static bool test_number(const char *p, const char *endp) {
+  if (&p[0] < endp && *p && strchr("0123456789+-._", *p)) {
+    return true;
+  }
+  if (&p[2] < endp) {
+    if (0 == memcmp(p, "nan", 3) || 0 == memcmp(p, "inf", 3)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+// Scan a literal that is not a string
+static int scan_nonstring_literal(scanner_t *sp, token_t *tok) {
+  int lineno = sp->lineno;
+  if (test_time(sp->cur, sp->endp)) {
+    return scan_time(sp, tok);
+  }
+
+  if (test_date(sp->cur, sp->endp)) {
+    return scan_timestamp(sp, tok);
+  }
+
+  if (test_bool(sp->cur, sp->endp)) {
+    return scan_bool(sp, tok);
+  }
+
+  if (test_number(sp->cur, sp->endp)) {
+    return scan_number(sp, tok);
+  }
+  return SETERROR(sp->ebuf, lineno, "invalid value");
+}
+
+// Return true if Unicode codepoint is allowed in a TOML 1.1 bare key.
+// Ranges taken verbatim from the TOML 1.1 spec grammar for bare-key-char.
+static bool is_unicode_bare_key_char(uint32_t cp) {
+  if (cp == 0xB2 || cp == 0xB3 || cp == 0xB9)
+    return true;
+  if (0xBC <= cp && cp <= 0xBE)
+    return true;
+  if (0xC0 <= cp && cp <= 0xD6)
+    return true;
+  if (0xD8 <= cp && cp <= 0xF6)
+    return true;
+  if (0xF8 <= cp && cp <= 0x37D)
+    return true;
+  if (0x37F <= cp && cp <= 0x1FFF)
+    return true;
+  if (cp == 0x200C || cp == 0x200D)
+    return true;
+  if (0x203F <= cp && cp <= 0x2040)
+    return true;
+  if (0x2070 <= cp && cp <= 0x218F)
+    return true;
+  if (0x2460 <= cp && cp <= 0x24FF)
+    return true;
+  if (0x2C00 <= cp && cp <= 0x2FEF)
+    return true;
+  if (0x3001 <= cp && cp <= 0xD7FF)
+    return true;
+  if (0xF900 <= cp && cp <= 0xFDCF)
+    return true;
+  if (0xFDF0 <= cp && cp <= 0xFFFD)
+    return true;
+  if (0x10000 <= cp && cp <= 0xEFFFF)
+    return true;
+  return false;
+}
+
+// Scan a literal
+static int scan_literal(scanner_t *sp, token_t *tok) {
+  *tok = mktoken(sp, TOK_LIT);
+  const char *p = sp->cur;
+  while (p < sp->endp) {
+    if (isalnum((unsigned char)*p) || *p == '_' || *p == '-') {
+      p++;
+      continue;
+    }
+    if ((unsigned char)*p >= 0x80) {
+      uint32_t cp;
+      int n = utf8_to_ucs(p, sp->endp - p, &cp);
+      if (n > 0 && is_unicode_bare_key_char(cp)) {
+        p += n;
+        continue;
+      }
+    }
+    break;
+  }
+  tok->str.len = p - tok->str.ptr;
+  sp->cur = p;
+  return 0;
+}
+
+// Save the current state of the scanner
+static scanner_state_t scan_mark(scanner_t *sp) {
+  scanner_state_t mark;
+  mark.sp = sp;
+  mark.cur = sp->cur;
+  mark.lineno = sp->lineno;
+  mark.line_start = sp->line_start;
+  return mark;
+}
+
+// Restore the scanner state to a previously saved state
+static void scan_restore(scanner_t *sp, scanner_state_t mark) {
+  assert(mark.sp == sp);
+  sp->cur = mark.cur;
+  sp->lineno = mark.lineno;
+  sp->line_start = mark.line_start;
+}
+
+// Return the next token
+static int scan_next(scanner_t *sp, bool keymode, token_t *tok) {
+  static const toktyp_t map[128] = {
+      ['\n'] = TOK_ENDL, ['.'] = TOK_DOT,    ['='] = TOK_EQUAL,
+      [','] = TOK_COMMA, ['{'] = TOK_LBRACE, ['}'] = TOK_RBRACE};
+again:
+  *tok = mktoken(sp, TOK_FIN);
+
+  int ch = S_GET();
+  if (ch == TOK_FIN) {
+    return 0;
+  }
+
+  tok->str.len = 1;
+  if (0 <= ch && ch < 128 && map[ch]) {
+    // map simple char to token type and done
+    tok->toktyp = map[ch];
+    return 0;
+  }
+
+  // handle char that require logic
+  switch (ch) {
+  case ' ':
+  case '\t':
+    goto again; // skip whitespace
+
+  case '#':
+    // comment: skip until newline
+    while (!S_MATCH('\n')) {
+      ch = S_GET();
+      if (ch == TOK_FIN)
+        break;
+      if ((0 <= ch && ch <= 0x8) || (0x0a <= ch && ch <= 0x1f) ||
+          (ch == 0x7f)) {
+        return SETERROR(sp->ebuf, sp->lineno, "bad control char in comment");
+      }
+    }
+    goto again; // skip comment
+
+  case '[':
+    tok->toktyp = TOK_LBRACK;
+    if (keymode && S_MATCH('[')) {
+      S_GET();
+      tok->toktyp = TOK_LLBRACK;
+      tok->str.len = 2;
+    }
+    break;
+
+  case ']':
+    tok->toktyp = TOK_RBRACK;
+    if (keymode && S_MATCH(']')) {
+      S_GET();
+      tok->toktyp = TOK_RRBRACK;
+      tok->str.len = 2;
+    }
+    break;
+
+  case '"':
+    sp->cur--;
+    DO(scan_string(sp, tok));
+    break;
+
+  case '\'':
+    sp->cur--;
+    DO(scan_litstring(sp, tok));
+    break;
+
+  default:
+    sp->cur--;
+    DO(keymode ? scan_literal(sp, tok) : scan_nonstring_literal(sp, tok));
+    break;
+  }
+
+  return 0;
+}
+
+// Check for stack overflow due to excessive number of brackets or braces
+static int check_overflow(scanner_t *sp, token_t *tok) {
+  switch (tok->toktyp) {
+  case TOK_LBRACK:
+    sp->bracket_level++;
+    if (sp->bracket_level > BRACKET_LEVEL_MAX) {
+      return SETERROR(sp->ebuf, sp->lineno, "stack overflow");
+    }
+    break;
+  case TOK_RBRACK:
+    sp->bracket_level--;
+    break;
+  case TOK_LBRACE:
+    sp->brace_level++;
+    if (sp->brace_level > BRACE_LEVEL_MAX) {
+      return SETERROR(sp->ebuf, sp->lineno, "stack overflow");
+    }
+    break;
+  case TOK_RBRACE:
+    sp->brace_level--;
+    break;
+  default:
+    break;
+  }
+  return 0;
+}
+
+static int scan_key(scanner_t *sp, token_t *tok) {
+  if (sp->errmsg) {
+    return -1;
+  }
+  if (scan_next(sp, true, tok) || check_overflow(sp, tok)) {
+    sp->errmsg = sp->ebuf.ptr;
+    return -1;
+  }
+  return 0;
+}
+
+static int scan_value(scanner_t *sp, token_t *tok) {
+  if (sp->errmsg) {
+    return -1;
+  }
+  if (scan_next(sp, false, tok) || check_overflow(sp, tok)) {
+    sp->errmsg = sp->ebuf.ptr;
+    return -1;
+  }
+  return 0;
+}
+
+/**
+ * Convert a char in utf8 into UCS, and store it in *ret.
+ * Return #bytes consumed or -1 on failure.
+ */
+static int utf8_to_ucs(const char *orig, int len, uint32_t *ret) {
+  const unsigned char *buf = (const unsigned char *)orig;
+  unsigned i = *buf++;
+  uint32_t v;
+
+  /* 0x00000000 - 0x0000007F:
+     0xxxxxxx
+  */
+  if (0 == (i >> 7)) {
+    if (len < 1)
+      return -1;
+    v = i;
+    return *ret = v, 1;
+  }
+  /* 0x00000080 - 0x000007FF:
+     110xxxxx 10xxxxxx
+  */
+  if (0x6 == (i >> 5)) {
+    if (len < 2)
+      return -1;
+    v = i & 0x1f;
+    for (int j = 0; j < 1; j++) {
+      i = *buf++;
+      if (0x2 != (i >> 6))
+        return -1;
+      v = (v << 6) | (i & 0x3f);
+    }
+    return *ret = v, (const char *)buf - orig;
+  }
+
+  /* 0x00000800 - 0x0000FFFF:
+     1110xxxx 10xxxxxx 10xxxxxx
+  */
+  if (0xE == (i >> 4)) {
+    if (len < 3)
+      return -1;
+    v = i & 0x0F;
+    for (int j = 0; j < 2; j++) {
+      i = *buf++;
+      if (0x2 != (i >> 6))
+        return -1;
+      v = (v << 6) | (i & 0x3f);
+    }
+    return *ret = v, (const char *)buf - orig;
+  }
+
+  /* 0x00010000 - 0x001FFFFF:
+     11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+  */
+  if (0x1E == (i >> 3)) {
+    if (len < 4)
+      return -1;
+    v = i & 0x07;
+    for (int j = 0; j < 3; j++) {
+      i = *buf++;
+      if (0x2 != (i >> 6))
+        return -1;
+      v = (v << 6) | (i & 0x3f);
+    }
+    return *ret = v, (const char *)buf - orig;
+  }
+
+  if (0) {
+    // NOTE: these code points taking more than 4 bytes are not supported
+
+    /* 0x00200000 - 0x03FFFFFF:
+       111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+    */
+    if (0x3E == (i >> 2)) {
+      if (len < 5)
+        return -1;
+      v = i & 0x03;
+      for (int j = 0; j < 4; j++) {
+        i = *buf++;
+        if (0x2 != (i >> 6))
+          return -1;
+        v = (v << 6) | (i & 0x3f);
+      }
+      return *ret = v, (const char *)buf - orig;
+    }
+
+    /* 0x04000000 - 0x7FFFFFFF:
+       1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+    */
+    if (0x7e == (i >> 1)) {
+      if (len < 6)
+        return -1;
+      v = i & 0x01;
+      for (int j = 0; j < 5; j++) {
+        i = *buf++;
+        if (0x2 != (i >> 6))
+          return -1;
+        v = (v << 6) | (i & 0x3f);
+      }
+      return *ret = v, (const char *)buf - orig;
+    }
+  }
+
+  return -1;
+}
+
+/**
+ * Convert a UCS char to utf8 code, and return it in buf.
+ * Return #bytes used in buf to encode the char, or
+ * -1 on error.
+ */
+static int ucs_to_utf8(uint32_t code, char buf[4]) {
+  /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16
+   */
+  /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well
+   * as 0xfffe and 0xffff (UCS noncharacters) should not appear in
+   * conforming UTF-8 streams.
+   */
+  /*
+   *  https://github.com/toml-lang/toml-test/issues/165
+   *  [0xd800, 0xdfff] and [0xfffe, 0xffff] are implicitly allowed by TOML, so
+   * we disable the check.
+   */
+  if (0) {
+    if (0xd800 <= code && code <= 0xdfff)
+      return -1;
+    if (0xfffe <= code && code <= 0xffff)
+      return -1;
+  }
+
+  /* 0x00000000 - 0x0000007F:
+     0xxxxxxx
+  */
+  if (code <= 0x7F) {
+    buf[0] = (unsigned char)code;
+    return 1;
+  }
+
+  /* 0x00000080 - 0x000007FF:
+     110xxxxx 10xxxxxx
+  */
+  if (code <= 0x000007FF) {
+    buf[0] = (unsigned char)(0xc0 | (code >> 6));
+    buf[1] = (unsigned char)(0x80 | (code & 0x3f));
+    return 2;
+  }
+
+  /* 0x00000800 - 0x0000FFFF:
+     1110xxxx 10xxxxxx 10xxxxxx
+  */
+  if (code <= 0x0000FFFF) {
+    buf[0] = (unsigned char)(0xe0 | (code >> 12));
+    buf[1] = (unsigned char)(0x80 | ((code >> 6) & 0x3f));
+    buf[2] = (unsigned char)(0x80 | (code & 0x3f));
+    return 3;
+  }
+
+  /* 0x00010000 - 0x001FFFFF:
+     11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+  */
+  if (code <= 0x001FFFFF) {
+    buf[0] = (unsigned char)(0xf0 | (code >> 18));
+    buf[1] = (unsigned char)(0x80 | ((code >> 12) & 0x3f));
+    buf[2] = (unsigned char)(0x80 | ((code >> 6) & 0x3f));
+    buf[3] = (unsigned char)(0x80 | (code & 0x3f));
+    return 4;
+  }
+
+#ifdef UNDEF
+  if (0) {
+    // NOTE: these code points taking more than 4 bytes are not supported
+    /* 0x00200000 - 0x03FFFFFF:
+       111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+    */
+    if (code <= 0x03FFFFFF) {
+      buf[0] = (unsigned char)(0xf8 | (code >> 24));
+      buf[1] = (unsigned char)(0x80 | ((code >> 18) & 0x3f));
+      buf[2] = (unsigned char)(0x80 | ((code >> 12) & 0x3f));
+      buf[3] = (unsigned char)(0x80 | ((code >> 6) & 0x3f));
+      buf[4] = (unsigned char)(0x80 | (code & 0x3f));
+      return 5;
+    }
+
+    /* 0x04000000 - 0x7FFFFFFF:
+       1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+    */
+    if (code <= 0x7FFFFFFF) {
+      buf[0] = (unsigned char)(0xfc | (code >> 30));
+      buf[1] = (unsigned char)(0x80 | ((code >> 24) & 0x3f));
+      buf[2] = (unsigned char)(0x80 | ((code >> 18) & 0x3f));
+      buf[3] = (unsigned char)(0x80 | ((code >> 12) & 0x3f));
+      buf[4] = (unsigned char)(0x80 | ((code >> 6) & 0x3f));
+      buf[5] = (unsigned char)(0x80 | (code & 0x3f));
+      return 6;
+    }
+  }
+#endif
+
+  return -1;
+}
diff --git a/src/include/common/tomlc17.h b/src/include/common/tomlc17.h
new file mode 100644
index 00000000000..1ba4c447869
--- /dev/null
+++ b/src/include/common/tomlc17.h
@@ -0,0 +1,277 @@
+/* Copyright (c) 2024-2026, CK Tan.
+ * https://github.com/cktan/tomlc17/blob/main/LICENSE
+ */
+
+/**
+ * @file tomlc17.h
+ * @brief A TOML parser for C17.
+ *
+ * This library provides a simple and efficient way to parse TOML documents
+ * in C. It supports standard TOML features and provides an easy-to-use API
+ * for traversing the parsed data.
+ */
+
+#ifndef TOMLC17_H
+#define TOMLC17_H
+
+// A crude way to determine version. Manually changed.
+#define TOMLC17_RELEASE_AFTER "260618"
+
+/*
+ *  USAGE:
+ *
+ *  1. Call toml_parse(), toml_parse_file(), or toml_parse_file_ex()
+ *  2. Check result.ok
+ *  3. Use toml_get() or toml_seek() to query and traverse the
+ *     result.toptab
+ *  4. Call toml_free() to release resources.
+ *
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#ifdef __cplusplus
+#define TOML_EXTERN extern "C"
+#else
+#define TOML_EXTERN extern
+#endif
+
+/**
+ * @brief Enumeration of TOML data types.
+ */
+enum toml_type_t {
+  TOML_UNKNOWN = 0, /**< Unknown or invalid type */
+  TOML_STRING,      /**< String type */
+  TOML_INT64,       /**< 64-bit integer type */
+  TOML_FP64,        /**< 64-bit floating point type */
+  TOML_BOOLEAN,     /**< Boolean type */
+  TOML_DATE,        /**< Local date type */
+  TOML_TIME,        /**< Local time type */
+  TOML_DATETIME,    /**< Local datetime type */
+  TOML_DATETIMETZ,  /**< Offset datetime type */
+  TOML_ARRAY,       /**< Array type */
+  TOML_TABLE,       /**< Table type */
+};
+typedef enum toml_type_t toml_type_t;
+
+/**
+ * @brief Represents a single piece of TOML data.
+ *
+ * This structure is a node in a tree that represents a TOML document.
+ * The `u` union contains the actual value based on the `type` field.
+ */
+typedef struct toml_datum_t toml_datum_t;
+struct toml_datum_t {
+  toml_type_t type;   /**< Type of the datum */
+  uint32_t flag;      /**< Internal flag, do not use */
+  int lineno;         /**< 1-based source line number, 0 if synthesized */
+  int colno;          /**< 1-based source column number, 0 if synthesized */
+  const char *source; /**< Source name (e.g. filename), NULL if not provided.
+                           Owned by the result; valid until toml_free(). */
+  union {
+    const char *s; /**< Shorthand for str.ptr */
+    struct {
+      const char *ptr; /**< NUL terminated string pointer */
+      int len; /**< Length of the string excluding the terminating NUL */
+    } str;
+    int64_t int64; /**< 64-bit integer value */
+    double fp64;   /**< 64-bit floating point value */
+    bool boolean;  /**< Boolean value */
+    struct {       /**< Date and time components */
+      int16_t year, month, day;
+      int16_t hour, minute, second;
+      int32_t usec;
+      int16_t tz; /**< Timezone offset in minutes */
+    } ts;
+    struct {              /**< Array data */
+      int32_t size;       /**< Number of elements in the array */
+      toml_datum_t *elem; /**< Array of elements */
+    } arr;
+    struct {               /**< Table data */
+      int32_t size;        /**< Number of keys in the table */
+      const char **key;    /**< Array of keys */
+      int *len;            /**< Array of key lengths */
+      toml_datum_t *value; /**< Array of values corresponding to keys */
+    } tab;
+  } u;
+};
+
+/**
+ * @brief Result of a TOML parsing operation.
+ */
+typedef struct toml_result_t toml_result_t;
+struct toml_result_t {
+  bool ok;             /**< True if parsing was successful */
+  toml_datum_t toptab; /**< The top-level table (valid if ok is true) */
+  char errmsg[200];    /**< Error message (valid if ok is false) */
+  void *__internal;    /**< Internal state, do not use */
+};
+
+/**
+ * @brief Parse a TOML document from a string.
+ *
+ * @param src A NUL-terminated string containing the TOML document.
+ * @param len The length of the string (excluding the NUL terminator).
+ * @return A toml_result_t structure. Must be freed with toml_free().
+ *
+ * IMPORTANT: src[] must be a NUL terminated string! The len parameter
+ * does not include the NUL terminator.
+ */
+TOML_EXTERN toml_result_t toml_parse(const char *src, int len);
+
+/**
+ * @brief Parse a TOML document, tagging every datum with a source name.
+ *
+ * @param src A NUL-terminated string containing the TOML document.
+ * @param len The length of the string (excluding the NUL terminator).
+ * @param name A source name (e.g. filename) copied into the result, or NULL.
+ *             Every parsed datum's `source` is set to this name (or NULL).
+ * @return A toml_result_t structure. Must be freed with toml_free().
+ *
+ * IMPORTANT: src[] must be a NUL terminated string! The len parameter
+ * does not include the NUL terminator.
+ */
+TOML_EXTERN toml_result_t toml_parse_named(const char *src, int len,
+                                           const char *name);
+
+/**
+ * @brief Parse a TOML document from a file pointer.
+ *
+ * @param fp A pointer to the open file. The caller is responsible for closing
+ * it.
+ * @return A toml_result_t structure. Must be freed with toml_free().
+ *
+ * IMPORTANT: you are still responsible to fclose(fp).
+ */
+TOML_EXTERN toml_result_t toml_parse_file(FILE *fp);
+
+/**
+ * @brief Parse a TOML document from a file pointer, tagging datums with a name.
+ *
+ * @param fp A pointer to the open file. The caller is responsible for closing
+ * it.
+ * @param name A source name copied into the result, or NULL.
+ * @return A toml_result_t structure. Must be freed with toml_free().
+ *
+ * IMPORTANT: you are still responsible to fclose(fp).
+ */
+TOML_EXTERN toml_result_t toml_parse_file_named(FILE *fp, const char *name);
+
+/**
+ * @brief Parse a TOML document from a file path.
+ *
+ * @param fname The path to the TOML file.
+ * @return A toml_result_t structure. Must be freed with toml_free().
+ */
+TOML_EXTERN toml_result_t toml_parse_file_ex(const char *fname);
+
+/**
+ * @brief Release resources allocated for a TOML result.
+ *
+ * @param result The TOML result to free.
+ */
+TOML_EXTERN void toml_free(toml_result_t result);
+
+/**
+ * @brief Find a value for a specific key in a TOML table.
+ *
+ * @param table The TOML table to search in.
+ * @param key The key to look for.
+ * @return The value associated with the key, or a datum with type TOML_UNKNOWN
+ * if not found.
+ */
+TOML_EXTERN toml_datum_t toml_get(toml_datum_t table, const char *key);
+
+/**
+ * @brief Locate a value using a multipart-key (e.g., "a.b.c").
+ *
+ * @param table The TOML table to start the search from.
+ * @param multipart_key A dot-separated key string. No escape characters
+ * allowed. Maximum length is 255 bytes.
+ * @return The value found, or a datum with type TOML_UNKNOWN if not found.
+ */
+TOML_EXTERN toml_datum_t toml_seek(toml_datum_t table,
+                                   const char *multipart_key);
+
+/**
+ * @brief OBSOLETE: use toml_get() instead.
+ * Find a key in a toml_table. Return the value of the key if found,
+ * or a TOML_UNKNOWN otherwise.
+ */
+static inline toml_datum_t toml_table_find(toml_datum_t table,
+                                           const char *key) {
+  return toml_get(table, key);
+}
+
+/**
+ * @brief Merge two TOML results.
+ *
+ * All results (r1, r2, and the returned result) must be freed independently.
+ *
+ * @param r1 The base TOML result.
+ * @param r2 The TOML result containing overrides.
+ * @return A new toml_result_t representing the merged document.
+ *
+ *  LOGIC:
+ *   ret = copy of r1
+ *   for each item x in r2:
+ *     if x is not in ret:
+ *         set x in ret
+ *     elif x in ret is NOT of the same type:
+ *         override
+ *     elif x in ret is an array of tables:
+ *         append r2.x to ret.x
+ *     elif x in ret is a table:
+ *         merge r2.x to ret.x
+ *     else:
+ *         override
+ */
+TOML_EXTERN toml_result_t toml_merge(const toml_result_t *r1,
+                                     const toml_result_t *r2);
+
+/**
+ * @brief Compare two TOML results for equality.
+ *
+ * Comparison is sensitive to the order of elements in arrays and tables.
+ *
+ * @param r1 The first TOML result.
+ * @param r2 The second TOML result.
+ * @return True if they are equivalent, false otherwise.
+ */
+TOML_EXTERN bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2);
+
+/**
+ * @brief Global options for the TOML parser.
+ */
+typedef struct toml_option_t toml_option_t;
+struct toml_option_t {
+  bool check_utf8; /**< If true, check if all characters are valid UTF-8.
+                      Default: false. */
+  void *(*mem_realloc)(
+      void *ptr,
+      size_t size); /**< Custom realloc function. Default: realloc(). */
+  void (*mem_free)(void *ptr); /**< Custom free function. Default: free(). */
+};
+
+/**
+ * @brief Get the default parser options.
+ *
+ * Use this to obtain and initialize a toml_option_t structure before
+ * customizing it.
+ *
+ * @return A toml_option_t with default values.
+ */
+TOML_EXTERN toml_option_t toml_default_option(void);
+
+/**
+ * @brief Set the global parser options.
+ *
+ * Call this only if you need to override the default behavior.
+ *
+ * @param opt The options to set.
+ */
+TOML_EXTERN void toml_set_option(toml_option_t opt);
+
+#endif // TOMLC17_H
-- 
2.43.0

