A fast, zero-copy EDN (Extensible Data Notation) reader written in C11 with SIMD acceleration.
EDN (Extensible Data Notation) is a data format similar to JSON, but richer and more extensible. Think of it as “JSON with superpowers”:
- JSON-like foundation: Maps
{:key value}, vectors[1 2 3], strings, numbers, booleans, null (nil) - Additional built-in types: Sets
#{:a :b}, keywords:keyword, symbolsmy-symbol, charactersnewline, lists(1 2 3) - Extensible via tagged literals:
#inst "2024-01-01",#uuid "..."—transform data at parse time with custom readers - Human-friendly: Comments, flexible whitespace, designed to be readable and writable by both humans and programs
- Language-agnostic: Originally from Clojure, but useful anywhere you need rich, extensible data interchange
Why EDN over JSON? More expressive types (keywords, symbols, sets), native extensibility through tags (no more {"__type": "Date", "value": "..."} hacks), and better support for configuration files and data interchange in functional programming environments.
Learn more: Official EDN specification
- 🚀 Fast: SIMD-accelerated parsing with NEON (ARM64), SSE4.2 (x86_64) and SIMD128 (WebAssembly) support
- 🌐 WebAssembly: Full WASM SIMD128 support for high-performance parsing in browsers and Node.js
- 💾 Zero-copy: Minimal allocations, references input data where possible
- 🎯 Simple API: Easy-to-use interface with comprehensive type support
- 🧹 Memory-safe: Arena allocator for efficient cleanup – single
edn_free()call - 🔧 Zero Dependencies: Pure C11 with standard library only
- ✅ Fully Tested: 340+ tests across 24 test suites
- 📖 UTF-8 Native: All string inputs and outputs are UTF-8 encoded
- 🏷️ Tagged Literals: Extensible data types with custom reader support
- 🗺️ Map Namespace Syntax: Clojure-compatible
#:ns{...}syntax (optional, disabled by default) - 🔤 Extended Characters:
formfeed,backspace, and octaloNNNliterals (optional, disabled by default) - 📝 Metadata: Clojure-style metadata
^{...}syntax (optional, disabled by default) - 📄 Text Blocks: Java-style multi-line text blocks
"""n...n"""(experimental, disabled by default) - 🔢 Ratio Numbers: Clojure-compatible ratio literals
22/7(optional, disabled by default) - 🔣 Extended Integers: Hex (
0xFF), octal (0777), binary (2r1010), and arbitrary radix (36rZZ) formats (optional, disabled by default) - 🔢 Underscore in Numeric Literals: Visual grouping with underscores
1_000_000,3.14_15_92,0xDE_AD_BE_EF(optional, disabled by default)
- C11 compatible compiler (GCC 4.9+, Clang 3.1+, MSVC 2015+)
- Make (Unix/macOS) or CMake (Windows/cross-platform)
- Supported platforms:
- macOS (Apple Silicon M1/M2/M3, Intel) – NEON/SSE4.2 SIMD
- Linux (ARM64, x86_64) – NEON/SSE4.2 SIMD
- Windows (x86_64, ARM64) – NEON/SSE4.2 SIMD via MSVC/MinGW/Clang
- WebAssembly – SIMD128 support for browsers and Node.js
Unix/macOS/Linux:
# Clone the repository
git clone https://github.com/DotFox/edn.c.git
cd edn.c
# Build static library (libedn.a)
make
# Run tests to verify build
make test
Windows:
# Clone the repository
git clone https://github.com/DotFox/edn.c.git
cd edn.c
# Build with CMake (works with MSVC, MinGW, Clang)
.build.bat
# Or use PowerShell script
.build.ps1 -Test
See docs/WINDOWS.md for detailed Windows build instructions.
Integrate Into Your Project
Option 1: Link static library
# Compile your code
gcc -o myapp myapp.c -I/path/to/edn.c/include -L/path/to/edn.c -ledn
# Or add to your Makefile
CFLAGS += -I/path/to/edn.c/include
LDFLAGS += -L/path/to/edn.c -ledn
Option 2: Include source directly
Copy include/edn.h and all files from src/ into your project and compile them together.
#include "edn.h"
#include
int main(void) {
const char *input = "{:name "Alice" :age 30 :languages [:clojure :rust]}";
// Read EDN string
edn_result_t result = edn_read(input, 0);
if (result.error != EDN_OK) {
fprintf(stderr, "Parse error at line %zu, column %zu: %sn",
result.error_line, result.error_column, result.error_message);
return 1;
}
// Access the parsed map
edn_value_t *map = result.value;
printf("Parsed map with %zu entriesn", edn_map_count(map));
// Look up a value by key
edn_result_t key_result = edn_read(":name", 0);
edn_value_t *name_value = edn_map_lookup(map, key_result.value);
if (name_value != NULL && edn_type(name_value) == EDN_TYPE_STRING) {
size_t len;
const char *name = edn_string_get(name_value, &len);
printf("Name: %.*sn", (int)len, name);
}
// Clean up - frees all allocated memory
edn_free(key_result.value);
edn_free(map);
return 0;
}
Output:
Parsed map with 3 entries
Name: Alice
Whitespace and Control Characters
EDN.C follows Clojure’s exact behavior for whitespace and control character handling:
The following characters act as whitespace delimiters (separate tokens):
| Character | Hex | Name | Common Use |
|---|---|---|---|
|
0x20 | Space | Standard spacing |
t |
0x09 | Tab | Indentation |
n |
0x0A | Line Feed (LF) | Unix line ending |
r |
0x0D | Carriage Return (CR) | Windows line ending |
f |
0x0C | Form Feed | Page break |
v |
0x0B | Vertical Tab | Vertical spacing |
, |
0x2C | Comma | Optional separator |
| FS | 0x1C | File Separator | Data separation |
| GS | 0x1D | Group Separator | Data separation |
| RS | 0x1E | Record Separator | Data separation |
| US | 0x1F | Unit Separator | Data separation |
Examples:
// All of these parse as vectors with 3 elements:
edn_read("[1 2 3]", 0); // spaces
edn_read("[1,2,3]", 0); // commas
edn_read("[1t2n3]", 0); // tabs and newlines
edn_read("[1f2x1C3]", 0); // formfeed and file separator
Control Characters in Identifiers
Control characters 0x00-0x1F (except whitespace delimiters) are valid in identifiers (symbols and keywords):
Valid identifier characters:
0x00–0x08: NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, Backspace0x0E–0x1B: Shift Out through Escape
Examples:
// Backspace in symbol - valid!
edn_result_t r = edn_read("[bfoo]", 0); // 1-element vector
edn_vector_count(r.value); // Returns 1
edn_free(r.value);
// Control characters in middle of identifier
const char input[] = {'[', 'f', 'o', 'o', 0x08, 'b', 'a', 'r', ']', 0};
r = edn_read(input, sizeof(input) - 1);
edn_vector_count(r.value); // Returns 1 (symbol: "foobbar")
edn_free(r.value);
// Versus whitespace - separates into 2 elements
edn_result_t r2 = edn_read("[footbar]", 0); // Tab is whitespace
edn_vector_count(r2.value); // Returns 2 (symbols: "foo" and "bar")
edn_free(r2.value);
Note on null bytes (0x00): When using string literals with strlen(), null bytes will truncate the string. Always pass explicit length for data containing null bytes:
const char data[] = {'[', 'a', 0x00, 'b', ']', 0};
edn_result_t r = edn_read(data, 5); // Pass exact length: 5 bytes (excluding terminator)
Read EDN from a UTF-8 string.
edn_result_t edn_read(const char *input, size_t length);
Parameters:
input: UTF-8 encoded string containing EDN data (must remain valid for zero-copy strings)length: Length of input in bytes, or0to usestrlen(input)
Returns: edn_result_t containing:
value: Parsed EDN value (NULL on error)error: Error code (EDN_OKon success)error_line,error_column: Error location (1-indexed)error_message: Human-readable error description
Important: The returned value must be freed with edn_free().
Free an EDN value and all associated memory.
void edn_free(edn_value_t *value);
Parameters:
value: Value to free (may be NULL)
Note: This frees the entire value tree. Do not call free() on individual values.
Get the type of an EDN value.
edn_type_t edn_type(const edn_value_t *value);
Returns: One of:
EDN_TYPE_NILEDN_TYPE_BOOLEDN_TYPE_INT(int64_t)EDN_TYPE_BIGINT(arbitrary precision integer)EDN_TYPE_FLOAT(double)EDN_TYPE_BIGDEC(exact precision decimal)EDN_TYPE_RATIO(rational number, requiresRATIO=1build flag)EDN_TYPE_CHARACTER(Unicode codepoint)EDN_TYPE_STRINGEDN_TYPE_SYMBOLEDN_TYPE_KEYWORDEDN_TYPE_LISTEDN_TYPE_VECTOREDN_TYPE_MAPEDN_TYPE_SETEDN_TYPE_TAGGED
typedef enum {
EDN_OK = 0, // Success
EDN_ERROR_INVALID_SYNTAX, // Syntax error
EDN_ERROR_UNEXPECTED_EOF, // Unexpected end of input
EDN_ERROR_OUT_OF_MEMORY, // Allocation failure
EDN_ERROR_INVALID_UTF8, // Invalid UTF-8 sequence
EDN_ERROR_INVALID_NUMBER, // Malformed number
EDN_ERROR_INVALID_STRING, // Malformed string
EDN_ERROR_INVALID_ESCAPE, // Invalid escape sequence
EDN_ERROR_UNMATCHED_DELIMITER, // Mismatched brackets
EDN_ERROR_UNKNOWN_TAG, // Unregistered tag (with ERROR mode)
EDN_ERROR_DUPLICATE_KEY, // Map has duplicate keys
EDN_ERROR_DUPLICATE_ELEMENT // Set has duplicate elements
} edn_error_t;
const char *edn_string_get(const edn_value_t *value, size_t *length);
Get UTF-8 string data. Returns NULL if value is not a string.
Lazy decoding: For strings without escapes, returns a pointer into the original input (zero-copy). For strings with escapes (n, t, ", etc.), decodes and caches the result on first call.
Example:
edn_result_t r = edn_read(""Hello, world!"", 0);
size_t len;
const char *str = edn_string_get(r.value, &len);
printf("%.*sn", (int)len, str);
edn_free(r.value);
bool edn_is_nil(const edn_value_t *value);
Check if value is nil. Returns true if value is EDN_TYPE_NIL, false otherwise.
Example:
edn_result_t r = edn_read("nil", 0);
if (edn_is_nil(r.value)) {
printf("Value is niln");
}
edn_free(r.value);
bool edn_bool_get(const edn_value_t *value, bool *out);
Get boolean value. Returns true if value is EDN_TYPE_BOOL, false otherwise.
Example:
edn_result_t r = edn_read("true", 0);
bool val;
if (edn_bool_get(r.value, &val)) {
printf("Boolean: %sn", val ? "true" : "false");
}
edn_free(r.value);
bool edn_int64_get(const edn_value_t *value, int64_t *out);
Get int64_t value. Returns true if value is EDN_TYPE_INT, false otherwise.
Example:
edn_result_t r = edn_read("42", 0);
int64_t num;
if (edn_int64_get(r.value, &num)) {
printf("Number: %lldn", (long long)num);
}
edn_free(r.value);
const char *edn_bigint_get(const edn_value_t *value, size_t *length,
bool *negative, uint8_t *radix);
Get big integer digit string for use with external libraries (GMP, OpenSSL BIGNUM, etc.).
Parameters:
value: EDN big integer valuelength: Output for digit string length (may be NULL)negative: Output for sign flag (may be NULL)radix: Output for number base: 10, 16, 8, or 2 (may be NULL)
Returns: Digit string, or NULL if not a big integer.
Clojure Compatibility: The N suffix forces BigInt for base-10 integers.
42N→ BigInt “42” (forced BigInt even though it fits in int64)999999999999999999999999999→ BigInt (overflow detection)0xDEADBEEFN→ Long (N is hex digit, not suffix)
Example:
// BigInt from overflow
edn_result_t r = edn_read("999999999999999999999999999", 0);
size_t len;
bool neg;
uint8_t radix;
const char *digits = edn_bigint_get(r.value, &len, &neg, &radix);
if (digits) {
printf("%s%.*s (base %d)n", neg ? "-" : "", (int)len, digits, radix);
}
edn_free(r.value);
// BigInt with N suffix
edn_result_t r2 = edn_read("42N", 0);
digits = edn_bigint_get(r2.value, &len, &neg, &radix);
// digits = "42", len = 2, use with GMP: mpz_set_str(bigint, digits, radix)
edn_free(r2.value);
bool edn_double_get(const edn_value_t *value, double *out);
Get double value. Returns true if value is EDN_TYPE_FLOAT, false otherwise.
bool edn_number_as_double(const edn_value_t *value, double *out);
Convert any numeric type (INT, BIGINT, FLOAT, BIGDEC) to double. May lose precision for large numbers.
Example:
edn_result_t r = edn_read("3.14159", 0);
double num;
if (edn_double_get(r.value, &num)) {
printf("Pi: %.5fn", num);
}
edn_free(r.value);
const char *edn_bigdec_get(const edn_value_t *value, size_t *length, bool *negative);
Get big decimal string for use with external libraries (Java BigDecimal, Python Decimal, etc.).
Parameters:
value: EDN big decimal valuelength: Output for string length (may be NULL)negative: Output for sign flag (may be NULL)
Returns: Decimal string, or NULL if not a big decimal.
Clojure Compatibility: The M suffix forces exact precision decimal representation.
42M→ BigDecimal “42” (integer with M suffix)3.14M→ BigDecimal “3.14”1.5e10M→ BigDecimal “1.5e10”
Example:
// BigDecimal from float
edn_result_t r1 = edn_read("3.14159265358979323846M", 0);
size_t len;
bool neg;
const char *decimal = edn_bigdec_get(r1.value, &len, &neg);
if (decimal) {
printf("%s%.*sn", neg ? "-" : "", (int)len, decimal);
// Use with: Java BigDecimal(decimal), Python Decimal(decimal), etc.
}
edn_free(r1.value);
// BigDecimal from integer with M suffix
edn_result_t r2 = edn_read("42M", 0);
decimal = edn_bigdec_get(r2.value, &len, &neg);
// decimal = "42", application can convert to BigDecimal
edn_free(r2.value);
bool edn_ratio_get(const edn_value_t *value, int64_t *numerator, int64_t *denominator);
Get ratio numerator and denominator. Returns true if value is EDN_TYPE_RATIO, false otherwise.
Parameters:
value: EDN ratio valuenumerator: Output for numerator (may be NULL)denominator: Output for denominator (may be NULL)
Returns: true if value is a ratio, false otherwise.
Clojure Compatibility: Ratios represent exact rational numbers as numerator/denominator pairs.
22/7→ Ratio with numerator=22, denominator=7-3/4→ Ratio with numerator=-3, denominator=4 (negative numerator)1/2→ Ratio with numerator=1, denominator=23/6→ Automatically reduced to ratio 1/210/5→ Automatically reduced to integer 2 (ratios with denominator 1 become integers)0/5→ Returns integer 0 (zero numerator always becomes integer 0)0777/3→ Returns 777/20777/0777→ Returns 1
Automatic Reduction:
Ratios are automatically reduced to lowest terms using the Binary GCD algorithm (Stein’s algorithm):
6/9→ Reduced to2/3100/25→ Reduced to4/1→ Returns as integer4
Restrictions:
- Only decimal (base-10) integers supported for both numerator and denominator
- Octal (base-8) integers supported keeping compatibility with Clojure where it is incorrectly interpreted as decimal integers with leading zeros.
- Both numerator and denominator must fit in
int64_t - Denominator must be positive (negative denominators are rejected)
- Denominator cannot be zero
- No whitespace allowed around
/ - Hex and binary notations not supported for ratios
Example:
// Parse ratio
edn_result_t r = edn_read("22/7", 0);
if (r.error == EDN_OK && edn_type(r.value) == EDN_TYPE_RATIO) {
int64_t num, den;
edn_ratio_get(r.value, &num, &den);
printf("Ratio: %lld/%lldn", (long long)num, (long long)den);
// Output: Ratio: 22/7
// Convert to double for approximation
double approx;
edn_number_as_double(r.value, &approx);
printf("Approximation: %.10fn", approx);
// Output: Approximation: 3.1428571429
}
edn_free(r.value);
// Automatic reduction
edn_result_t r2 = edn_read("3/6", 0);
int64_t num2, den2;
edn_ratio_get(r2.value, &num2, &den2);
// num2 = 1, den2 = 2 (reduced from 3/6)
edn_free(r2.value);
// Reduction to integer
edn_result_t r3 = edn_read("10/5", 0);
assert(edn_type(r3.value) == EDN_TYPE_INT);
int64_t int_val;
edn_int64_get(r3.value, &int_val);
// int_val = 2 (10/5 reduced to 2/1, returned as integer)
edn_free(r3.value);
// Negative ratios
edn_result_t r4 = edn_read("-3/4", 0);
int64_t num4, den4;
edn_ratio_get(r4.value, &num4, &den4);
// num4 = -3, den4 = 4 (numerator is negative, denominator is positive)
edn_free(r4.value);
// Error: zero denominator
edn_result_t r5 = edn_read("5/0", 0);
// r5.error == EDN_ERROR_INVALID_NUMBER
// r5.error_message == "Ratio denominator cannot be zero"
// Error: negative denominator (denominators must be positive)
edn_result_t r6 = edn_read("3/-4", 0);
// r6.error == EDN_ERROR_INVALID_NUMBER
// r6.error_message == "Ratio denominator must be positive"
// Error: hex not supported
edn_result_t r7 = edn_read("0x10/2", 0);
// Parses 0x10 as int, not as ratio
Build Configuration:
This feature is disabled by default. To enable it:
Make:
CMake:
cmake -DEDN_ENABLE_RATIO=ON ..
make
When disabled (default):
EDN_TYPE_RATIOenum value is not availableedn_ratio_get()function is not available
Note: Ratios are a Clojure language feature, not part of the official EDN specification. They’re provided here for compatibility with Clojure’s clojure.edn parser.
See test/test_numbers.c for comprehensive ratio test examples.
EDN.C supports Clojure-style special integer formats for hexadecimal, octal, binary, and arbitrary radix numbers. These are disabled by default as they are not part of the base EDN specification.
Supported formats:
- Hexadecimal:
0xFF,0x2A,-0x10(base-16, prefix0xor0X) - Octal:
0777,052,-0123(base-8, leading zero followed by0-7) - Binary:
2r1010,-2r1111(base-2, radix notation) - Arbitrary radix:
8r77,16rFF,36rZZ(bases 2-36, radix notationNrDDDD)
Examples:
// Hexadecimal
edn_result_t r1 = edn_read("0xFF", 0);
int64_t val1;
edn_int64_get(r1.value, &val1);
// val1 = 255
edn_free(r1.value);
// Octal
edn_result_t r2 = edn_read("0777", 0);
int64_t val2;
edn_int64_get(r2.value, &val2);
// val2 = 511 (7*64 + 7*8 + 7)
edn_free(r2.value);
// Binary (radix notation)
edn_result_t r3 = edn_read("2r1010", 0);
int64_t val3;
edn_int64_get(r3.value, &val3);
// val3 = 10
edn_free(r3.value);
// Base-36 (radix notation)
edn_result_t r4 = edn_read("36rZZ", 0);
int64_t val4;
edn_int64_get(r4.value, &val4);
// val4 = 1295 (35*36 + 35)
edn_free(r4.value);
// Negative hex
edn_result_t r5 = edn_read("-0x10", 0);
int64_t val5;
edn_int64_get(r5.value, &val5);
// val5 = -16
edn_free(r5.value);
Radix notation: NrDDDD where:
Nis the radix (base) from 2 to 36ris the radix separatorDDDDare the digits (0-9, A-Z, case-insensitive for bases > 10)
Build Configuration:
This feature is disabled by default. To enable it:
Make:
CMake:
cmake -DEDN_ENABLE_EXTENDED_INTEGERS=ON ..
make
When disabled (default):
- Hexadecimal (
0xFF), binary (2r1010), and radix notation (36rZZ) will fail to parse - Leading zeros are forbidden: Numbers like
01,0123,0777are rejected (per EDN spec) - Only
0itself, or0.5,0e10(floats starting with zero) are allowed
Note: Extended integer formats are a Clojure language feature, not part of the official EDN specification. They’re provided here for compatibility with Clojure’s reader.
See test/test_numbers.c for comprehensive extended integer format test examples.
Underscore in Numeric Literals
EDN.C supports underscores as visual separators in numeric literals for improved readability. This feature is disabled by default as it’s not part of the base EDN specification.
Supported number types:
- Integers:
1_000,1_000_000,4____2→1000,1000000,42 - Floats:
3.14_15_92,1_234.56_78→3.141592,1234.5678 - Scientific notation:
1_500e10,1.5e1_0,1_5.2_5e1_0→1500e10,1.5e10,15.25e10 - BigInt:
1_234_567_890_123_456_789N - BigDecimal:
1_234.56_78M,1_5.2_5e1_0M - Hexadecimal (with
EXTENDED_INTEGERS=1):0xDE_AD_BE_EF→0xDEADBEEF - Octal (with
EXTENDED_INTEGERS=1):07_77→0777 - Binary (with
EXTENDED_INTEGERS=1):2r1010_1010→170 - Radix notation (with
EXTENDED_INTEGERS=1):36rZ_Z→1295
Rules:
- Underscores are only allowed between digits (not at start, end, or adjacent to special characters)
- Multiple consecutive underscores are allowed:
4____2is valid - Not allowed adjacent to decimal point:
123_.5or123._5are invalid - Not allowed before/after exponent marker:
123_e10or123e_10are invalid - Not allowed before suffix:
123_Nor123.45_Mare invalid - Works with negative numbers:
-1_234→-1234
Examples:
// Credit card number formatting
edn_result_t r1 = edn_read("1234_5678_9012_3456", 0);
int64_t val1;
edn_int64_get(r1.value, &val1);
// val1 = 1234567890123456
edn_free(r1.value);
// Pi with digit grouping
edn_result_t r2 = edn_read("3.14_15_92_65_35_89_79", 0);
double val2;
edn_double_get(r2.value, &val2);
// val2 = 3.141592653589793
edn_free(r2.value);
// Hex bytes (requires EXTENDED_INTEGERS=1)
edn_result_t r3 = edn_read("0xFF_EC_DE_5E", 0);
int64_t val3;
edn_int64_get(r3.value, &val3);
// val3 = 0xFFECDE5E
edn_free(r3.value);
// Large numbers with thousands separators
edn_result_t r4 = edn_read("1_000_000", 0);
int64_t val4;
edn_int64_get(r4.value, &val4);
// val4 = 1000000
edn_free(r4.value);
// In collections
edn_result_t r5 = edn_read("[1_000 2_000 3_000]", 0);
// Three integers: 1000, 2000, 3000
edn_free(r5.value);
Invalid examples:
// Underscore at start - parses as symbol
edn_read("_123", 0); // Symbol, not number
// Underscore at end
edn_read("123_", 0); // Error: EDN_ERROR_INVALID_NUMBER
// Adjacent to decimal point
edn_read("123_.5", 0); // Error: EDN_ERROR_INVALID_NUMBER
edn_read("123._5", 0); // Error: EDN_ERROR_INVALID_NUMBER
// Before/after exponent marker
edn_read("123_e10", 0); // Error: EDN_ERROR_INVALID_NUMBER
edn_read("123e_10", 0); // Error: EDN_ERROR_INVALID_NUMBER
// Before suffix
edn_read("123_N", 0); // Error: EDN_ERROR_INVALID_NUMBER
edn_read("123.45_M", 0); // Error: EDN_ERROR_INVALID_NUMBER
Build Configuration:
This feature is disabled by default. To enable it:
Make:
make UNDERSCORE_IN_NUMERIC=1
CMake:
cmake -DEDN_ENABLE_UNDERSCORE_IN_NUMERIC=ON ..
make
Combined with other features:
# Enable underscores with extended integers and ratios
make UNDERSCORE_IN_NUMERIC=1 EXTENDED_INTEGERS=1 RATIO=1
When disabled (default):
- Numbers with underscores will fail to parse
- The scanner will stop at the first underscore, treating it as an invalid number
Note: Underscores in numeric literals are a common feature in modern programming languages (Java, Rust, Python 3.6+, etc.) but are not part of the official EDN specification. This feature is provided for convenience and readability.
See test/test_underscore_numeric.c for comprehensive test examples.
bool edn_character_get(const edn_value_t *value, uint32_t *out);
Get Unicode codepoint. Returns true if value is EDN_TYPE_CHARACTER, false otherwise.
Example:
// Named characters: newline, tab, space, return
edn_result_t r1 = edn_read("\newline", 0);
uint32_t cp1;
edn_character_get(r1.value, &cp1); // cp1 = 0x0A
// Unicode: uXXXX or literal character
edn_result_t r2 = edn_read("\u03B1", 0); // Greek alpha
uint32_t cp2;
edn_character_get(r2.value, &cp2); // cp2 = 0x03B1
edn_free(r1.value);
edn_free(r2.value);
Convenience functions for type checking:
bool edn_is_nil(const edn_value_t *value);
bool edn_is_string(const edn_value_t *value);
bool edn_is_number(const edn_value_t *value);
bool edn_is_integer(const edn_value_t *value);
bool edn_is_collection(const edn_value_t *value);
Type predicate details:
edn_is_nil()– ReturnstrueforEDN_TYPE_NILedn_is_string()– ReturnstrueforEDN_TYPE_STRINGedn_is_number()– Returnstruefor any numeric type (INT, BIGINT, FLOAT, BIGDEC, RATIO)edn_is_integer()– Returnstruefor integer types (INT, BIGINT)edn_is_collection()– Returnstruefor collections (LIST, VECTOR, MAP, SET)
Example:
edn_result_t r = edn_read("[42 "hello" [1 2] {:a 1}]", 0);
if (edn_is_collection(r.value)) {
for (size_t i = 0; i < edn_vector_count(r.value); i++) {
edn_value_t* elem = edn_vector_get(r.value, i);
if (edn_is_number(elem)) {
printf("Found numbern");
} else if (edn_is_string(elem)) {
printf("Found stringn");
} else if (edn_is_collection(elem)) {
printf("Found nested collectionn");
}
}
}
edn_free(r.value);
bool edn_string_equals(const edn_value_t *value, const char *str);
Compare EDN string with C string for equality. Returns true if equal, false otherwise.
Example:
edn_result_t r = edn_read("{:status "active"}", 0);
edn_value_t* status = edn_map_get_keyword(r.value, "status");
if (edn_string_equals(status, "active")) {
printf("Status is activen");
}
edn_free(r.value);
bool edn_symbol_get(const edn_value_t *value,
const char **namespace, size_t *ns_length,
const char **name, size_t *name_length);
Get symbol components. Returns true if value is EDN_TYPE_SYMBOL, false otherwise.
Example:
// Simple symbol
edn_result_t r1 = edn_read("foo", 0);
const char *name;
size_t name_len;
edn_symbol_get(r1.value, NULL, NULL, &name, &name_len);
printf("Symbol: %.*sn", (int)name_len, name);
// Namespaced symbol
edn_result_t r2 = edn_read("clojure.core/map", 0);
const char *ns, *n;
size_t ns_len, n_len;
edn_symbol_get(r2.value, &ns, &ns_len, &n, &n_len);
printf("Symbol: %.*s/%.*sn", (int)ns_len, ns, (int)n_len, n);
edn_free(r1.value);
edn_free(r2.value);
bool edn_keyword_get(const edn_value_t *value,
const char **namespace, size_t *ns_length,
const char **name, size_t *name_length);
Get keyword components. Returns true if value is EDN_TYPE_KEYWORD, false otherwise.
Example:
edn_result_t r = edn_read(":name", 0);
const char *name;
size_t name_len;
edn_keyword_get(r.value, NULL, NULL, &name, &name_len);
printf("Keyword: :%.*sn", (int)name_len, name);
edn_free(r.value);
Ordered sequences: (1 2 3)
size_t edn_list_count(const edn_value_t *value);
edn_value_t *edn_list_get(const edn_value_t *value, size_t index);
Example:
edn_result_t r = edn_read("(1 2 3)", 0);
size_t count = edn_list_count(r.value);
for (size_t i = 0; i < count; i++) {
edn_value_t *elem = edn_list_get(r.value, i);
int64_t num;
if (edn_int64_get(elem, &num)) {
printf("%lld ", (long long)num);
}
}
printf("n");
edn_free(r.value);
Indexed sequences: [1 2 3]
size_t edn_vector_count(const edn_value_t *value);
edn_value_t *edn_vector_get(const edn_value_t *value, size_t index);
Example:
edn_result_t r = edn_read("["a" "b" "c"]", 0);
size_t count = edn_vector_count(r.value);
for (size_t i = 0; i < count; i++) {
edn_value_t *elem = edn_vector_get(r.value, i);
size_t len;
const char *str = edn_string_get(elem, &len);
printf("[%zu] = %.*sn", i, (int)len, str);
}
edn_free(r.value);
Unique elements: #{:a :b :c}
size_t edn_set_count(const edn_value_t *value);
edn_value_t *edn_set_get(const edn_value_t *value, size_t index);
bool edn_set_contains(const edn_value_t *value, const edn_value_t *element);
Note: Sets reject duplicate elements during parsing. Iteration order is implementation-defined.
Example:
edn_result_t r = edn_read("#{:a :b :c}", 0);
printf("Set has %zu elementsn", edn_set_count(r.value));
edn_result_t key = edn_read(":a", 0);
if (edn_set_contains(r.value, key.value)) {
printf(":a is in setn");
}
edn_free(key.value);
edn_free(r.value);
Key-value pairs: {:foo 1 :bar 2}
size_t edn_map_count(const edn_value_t *value);
edn_value_t *edn_map_get_key(const edn_value_t *value, size_t index);
edn_value_t *edn_map_get_value(const edn_value_t *value, size_t index);
edn_value_t *edn_map_lookup(const edn_value_t *value, const edn_value_t *key);
bool edn_map_contains_key(const edn_value_t *value, const edn_value_t *key);
Note: Maps reject duplicate keys during parsing. Iteration order is implementation-defined.
Example:
edn_result_t r = edn_read("{:name "Alice" :age 30}", 0);
// Iterate over all entries
size_t count = edn_map_count(r.value);
for (size_t i = 0; i < count; i++) {
edn_value_t *key = edn_map_get_key(r.value, i);
edn_value_t *val = edn_map_get_value(r.value, i);
const char *key_name;
size_t key_len;
edn_keyword_get(key, NULL, NULL, &key_name, &key_len);
printf(":%.*s => ", (int)key_len, key_name);
if (edn_type(val) == EDN_TYPE_STRING) {
size_t val_len;
const char *str = edn_string_get(val, &val_len);
printf(""%.*s"n", (int)val_len, str);
} else if (edn_type(val) == EDN_TYPE_INT) {
int64_t num;
edn_int64_get(val, &num);
printf("%lldn", (long long)num);
}
}
// Lookup by key
edn_result_t key = edn_read(":name", 0);
edn_value_t *name = edn_map_lookup(r.value, key.value);
if (name != NULL) {
size_t len;
const char *str = edn_string_get(name, &len);
printf("Name: %.*sn", (int)len, str);
}
edn_free(key.value);
edn_free(r.value);
Map Convenience Functions:
edn_value_t *edn_map_get_keyword(const edn_value_t *map, const char *keyword);
edn_value_t *edn_map_get_namespaced_keyword(const edn_value_t *map, const char *namespace, const char *name);
edn_value_t *edn_map_get_string_key(const edn_value_t *map, const char *key);
Convenience wrappers that simplify common map lookup patterns by creating the key internally.
Example:
edn_result_t r = edn_read("{:name "Alice" :family/name "Black" :age 30 "config" true}", 0);
// Keyword lookup
edn_value_t* name = edn_map_get_keyword(r.value, "name");
if (name && edn_is_string(name)) {
// name is "Alice"
}
edn_value_t* name = edn_map_get_namespaced_keyword(r.value, "family", "name");
if (name && edn_is_string(name)) {
// name is "Black"
}
// String key lookup
edn_value_t* config = edn_map_get_string_key(r.value, "config");
if (config) {
bool val;
edn_bool_get(config, &val); // val is true
}
edn_free(r.value);
Tagged literals provide extensibility: #tag value
Basic Tagged Literal Access
bool edn_tagged_get(const edn_value_t *value,
const char **tag, size_t *tag_length,
edn_value_t **tagged_value);
Example:
edn_result_t r = edn_read("#inst "2024-01-01T00:00:00Z"", 0);
const char *tag;
size_t tag_len;
edn_value_t *wrapped;
if (edn_tagged_get(r.value, &tag, &tag_len, &wrapped)) {
printf("Tag: %.*sn", (int)tag_len, tag);
size_t str_len;
const char *str = edn_string_get(wrapped, &str_len);
printf("Value: %.*sn", (int)str_len, str);
}
edn_free(r.value);
Transform tagged literals during parsing with custom reader functions.
Reader Registry Functions
// Create and destroy registry
edn_reader_registry_t *edn_reader_registry_create(void);
void edn_reader_registry_destroy(edn_reader_registry_t *registry);
// Register/unregister readers
bool edn_reader_register(edn_reader_registry_t *registry,
const char *tag, edn_reader_fn reader);
void edn_reader_unregister(edn_reader_registry_t *registry, const char *tag);
edn_reader_fn edn_reader_lookup(const edn_reader_registry_t *registry,
const char *tag);
typedef edn_value_t *(*edn_reader_fn)(edn_value_t *value,
edn_arena_t *arena,
const char **error_message);
A reader function receives the wrapped value and transforms it into a new representation. On error, set error_message to a static string and return NULL.
typedef struct {
edn_reader_registry_t *reader_registry; // Optional reader registry
edn_value_t *eof_value; // Optional value to return on EOF
edn_default_reader_mode_t default_reader_mode;
} edn_parse_options_t;
edn_result_t edn_read_with_options(const char *input, size_t length,
const edn_parse_options_t *options);
Parse options fields:
reader_registry: Optional reader registry for tagged literal transformationseof_value: Optional value to return when EOF is encountered instead of an errordefault_reader_mode: Behavior for unregistered tags (see below)
Default reader modes:
EDN_DEFAULT_READER_PASSTHROUGH: ReturnEDN_TYPE_TAGGEDfor unregistered tags (default)EDN_DEFAULT_READER_UNWRAP: Discard tag, return wrapped valueEDN_DEFAULT_READER_ERROR: Fail withEDN_ERROR_UNKNOWN_TAG
EOF Value Handling:
By default, when the parser encounters end-of-file (empty input, whitespace-only input, or after #_ discard), it returns EDN_ERROR_UNEXPECTED_EOF. You can customize this behavior by providing an eof_value in the parse options:
// First, create an EOF sentinel value
edn_result_t eof_sentinel = edn_read(":eof", 0);
// Configure parse options with EOF value
edn_parse_options_t options = {
.reader_registry = NULL,
.eof_value = eof_sentinel.value,
.default_reader_mode = EDN_DEFAULT_READER_PASSTHROUGH
};
// Parse input that results in EOF
edn_result_t result = edn_read_with_options(" ", 3, &options);
// Instead of EDN_ERROR_UNEXPECTED_EOF, returns EDN_OK with eof_value
if (result.error == EDN_OK) {
// result.value == eof_sentinel.value
const char* name;
edn_keyword_get(result.value, NULL, NULL, &name, NULL);
// name == "eof"
}
// Clean up
edn_free(eof_sentinel.value);
