set(EAST_C_SOURCES
    src/arena.c
    src/cpu_count.c
    src/gc.c
    src/hashmap.c
    src/types.c
    src/values.c
    src/value_slab.c
    src/ir.c
    src/ir_resolve.c
    src/env.c
    src/compiler.c
    src/platform.c
    src/builtins/registry.c
    src/builtins/integer.c
    src/builtins/float_ops.c
    src/builtins/boolean.c
    src/builtins/string.c
    src/builtins/comparison.c
    src/builtins/datetime_ops.c
    src/builtins/blob.c
    src/builtins/array.c
    src/builtins/set_ops.c
    src/builtins/dict_ops.c
    src/builtins/ref_ops.c
    src/builtins/vector.c
    src/builtins/matrix.c
    src/builtins/patch.c
    src/serialization/json.c
    src/serialization/beast.c
    src/serialization/beast2/tags.c
    src/serialization/beast2/type_table.c
    src/serialization/beast2/full.c
    src/serialization/beast2/v4/string_table.c
    src/serialization/beast2/v4/backref.c
    src/serialization/beast2/v4/dedup.c
    src/serialization/beast2/v4/value_encode.c
    src/serialization/beast2/v4/value_decode.c
    src/serialization/beast2/v4/container.c
    src/serialization/beast2/v4/sourcemap_table.c
    src/serialization/beast2/v4/value_table.c
    src/serialization/beast2/v5/deflate.c
    src/serialization/beast2/v5/codec.c
    src/serialization/beast2/v5/container.c
    src/serialization/beast2/v5/stream.c
    src/serialization/beast2/v5/project.c
    src/serialization/csv.c
    src/serialization/east_parser.c
    src/serialization/east_printer.c
    src/serialization/binary_utils.c
    src/type_of_type.c
    src/source_map.c
    src/ir_normalize.c
)

add_library(east-c ${EAST_C_SOURCES})

# Keep FP contraction off: a fused multiply-add rounds once where the
# reference TypeScript runtime always rounds twice, so a contracted a*b+c
# (VectorAddScaled, VectorDot, SparseAxpy, ...) diverges from east-node in
# the last bit. GCC and Clang contract by default wherever hardware FMA is
# baseline (arm64). Set on the target so it also applies when east-py builds
# east-c via add_subdirectory (which bypasses the root CMakeLists options).
# MSVC has no -ffp-contract; it needs /fp:strict or `#pragma fp_contract`
# when the MSVC build lands.
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
    target_compile_options(east-c PRIVATE -ffp-contract=off)
endif()

target_include_directories(east-c PUBLIC include)
target_link_libraries(east-c PRIVATE pcre2-8)
# The beast2 v5 writer deflates frames on a worker pool (issue #763). PUBLIC:
# compat.h's thread shim is header-inline, so every consumer of the library
# links the platform thread library too (a no-op on Windows).
find_package(Threads REQUIRED)
target_link_libraries(east-c PUBLIC Threads::Threads)
# values.c backs Set/Dict with tidwall/btree.c. Parents that add_subdirectory()
# this package (standalone east-c, east-py, and each downstream east-py package)
# may already provide the btreec target; declare it here when they haven't so
# every consumer resolves btree.h without re-stating the dependency. btreec
# carries btree.h's include dir + BTREE_NOATOMICS as PUBLIC usage requirements.
if(NOT TARGET btreec)
    include(FetchContent)
    FetchContent_Declare(
        btreec
        GIT_REPOSITORY https://github.com/tidwall/btree.c.git
        GIT_TAG v0.6.4
    )
    FetchContent_GetProperties(btreec)
    if(NOT btreec_POPULATED)
        FetchContent_Populate(btreec)
    endif()
    add_library(btreec STATIC ${btreec_SOURCE_DIR}/btree.c)
    target_include_directories(btreec PUBLIC ${btreec_SOURCE_DIR})
    target_compile_definitions(btreec PUBLIC BTREE_NOATOMICS)
    if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        target_compile_options(btreec PRIVATE -w)
    endif()
endif()
target_link_libraries(east-c PRIVATE btreec)
# beast2 v5's baseline frame codec is raw DEFLATE, provided by the vendored
# single-file miniz (zlib-compatible streams, no system dependency — builds
# under MSVC/Ninja on windows-latest unchanged). Declared with the same
# defensive if(NOT TARGET) shape as btreec so east-py's direct
# add_subdirectory of this package resolves it too.
if(NOT TARGET miniz)
    include(FetchContent)
    FetchContent_Declare(
        miniz
        URL https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip
        URL_HASH SHA256=ada38db0b703a56d3dd6d57bf84a9c5d664921d870d8fea4db153979fb5332c5
    )
    FetchContent_GetProperties(miniz)
    if(NOT miniz_POPULATED)
        FetchContent_Populate(miniz)
    endif()
    add_library(miniz STATIC ${miniz_SOURCE_DIR}/miniz.c)
    target_include_directories(miniz PUBLIC ${miniz_SOURCE_DIR})
    if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        target_compile_options(miniz PRIVATE -w)
    endif()
endif()
target_link_libraries(east-c PRIVATE miniz)
# Whole frames inflate through libdeflate, several times faster than miniz's
# tinfl on the decode direction — the paged read path's largest fixed cost.
# Decompression only (its compression, zlib and gzip halves stay out); the
# deterministic encoder is beast2's own (v5/deflate.c), and the fence probes'
# bounded prefix inflate stays on tinfl, which streams. Built from its
# sources like btreec and miniz above, with the same defensive guard.
if(NOT TARGET libdeflate)
    include(FetchContent)
    FetchContent_Declare(
        libdeflate
        URL https://github.com/ebiggers/libdeflate/archive/refs/tags/v1.26.tar.gz
        URL_HASH SHA256=bba03fffc5538576213675ce6968fcff6ce2e67d82e4d5febea2d05f9f13cf85
        DOWNLOAD_EXTRACT_TIMESTAMP TRUE
    )
    FetchContent_GetProperties(libdeflate)
    if(NOT libdeflate_POPULATED)
        FetchContent_Populate(libdeflate)
    endif()
    add_library(libdeflate STATIC
        ${libdeflate_SOURCE_DIR}/lib/deflate_decompress.c
        ${libdeflate_SOURCE_DIR}/lib/utils.c
        ${libdeflate_SOURCE_DIR}/lib/arm/cpu_features.c
        ${libdeflate_SOURCE_DIR}/lib/x86/cpu_features.c)
    set_target_properties(libdeflate PROPERTIES OUTPUT_NAME deflate)
    target_include_directories(libdeflate PUBLIC ${libdeflate_SOURCE_DIR})
    if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        target_compile_options(libdeflate PRIVATE -w)
    endif()
endif()
target_link_libraries(east-c PRIVATE libdeflate)
# libm is part of the CRT on Windows/MSVC; only link the separate libm elsewhere.
if(NOT WIN32)
    target_link_libraries(east-c PRIVATE m)
endif()
target_compile_definitions(east-c PRIVATE PCRE2_CODE_UNIT_WIDTH=8)
# Build east-c with the POSIX CRT names (strdup, ...) without C4996 deprecation
# warnings. Set on the target so it also applies when east-py builds east-c via
# add_subdirectory (which bypasses the root CMakeLists force-include below).
if(WIN32)
    target_compile_definitions(east-c PRIVATE _CRT_NONSTDC_NO_WARNINGS _CRT_SECURE_NO_WARNINGS)
    # Force-include the compat shim so the POSIX / atomic / __thread mappings
    # reach every core TU even when east-py builds east-c via add_subdirectory
    # (which bypasses the root CMakeLists force-include). MSVC: /FI; else -include.
    if(MSVC)
        target_compile_options(east-c PRIVATE /FI "${CMAKE_CURRENT_SOURCE_DIR}/include/east/compat.h")
    else()
        target_compile_options(east-c PRIVATE -include "${CMAKE_CURRENT_SOURCE_DIR}/include/east/compat.h")
    endif()
endif()

if(EAST_USE_MIMALLOC AND TARGET mimalloc-static)
    target_link_libraries(east-c PUBLIC mimalloc-static)
    target_compile_definitions(east-c PUBLIC EAST_USE_MIMALLOC=1)
endif()

# Opt-in OBJECT-library twin of east-c, requested by east-py via EAST_C_BUILD_OBJECT.
# east-py links east-c as ONE shared library so the whole process has a single
# value slab. On Windows that shared library is a DLL whose export table is
# auto-generated (WINDOWS_EXPORT_ALL_SYMBOLS) by scanning the target's OWN object
# files — it never scans a static-archive dependency's members, even one pulled in
# with /WHOLEARCHIVE. So the DLL must own east-c's objects directly: build them
# here as an OBJECT library and splice $<TARGET_OBJECTS:east-c-obj> into the SHARED
# target in east-py's CMakeLists. The STATIC east-c above is untouched (the
# standalone CLI + test/profile executables keep linking it). Opt-in so a plain CLI
# build doesn't compile every TU twice; the source list is shared so they can't drift.
if(EAST_C_BUILD_OBJECT)
    add_library(east-c-obj OBJECT ${EAST_C_SOURCES})
    target_include_directories(east-c-obj PUBLIC include)
    # Threads: the frame pool's thread shim, as on the static target above.
    target_link_libraries(east-c-obj PRIVATE pcre2-8 btreec miniz libdeflate Threads::Threads)
    if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        target_compile_options(east-c-obj PRIVATE -ffp-contract=off)
    endif()
    # Mark east-c's extern DATA singletons (the type/value globals) dllexport so
    # they're in the DLL's export table; consumers see them as dllimport. Functions
    # auto-export via WINDOWS_EXPORT_ALL_SYMBOLS and need no marker. (EAST_DATA is a
    # no-op off Windows, so this define is harmless on the non-Windows object build.)
    target_compile_definitions(east-c-obj PRIVATE EAST_C_DLL_EXPORTS)
    target_compile_definitions(east-c-obj PRIVATE PCRE2_CODE_UNIT_WIDTH=8)
    if(WIN32)
        target_compile_definitions(east-c-obj PRIVATE _CRT_NONSTDC_NO_WARNINGS _CRT_SECURE_NO_WARNINGS)
        if(MSVC)
            target_compile_options(east-c-obj PRIVATE /FI "${CMAKE_CURRENT_SOURCE_DIR}/include/east/compat.h")
        else()
            target_compile_options(east-c-obj PRIVATE -include "${CMAKE_CURRENT_SOURCE_DIR}/include/east/compat.h")
        endif()
    endif()
    if(EAST_USE_MIMALLOC AND TARGET mimalloc-static)
        target_link_libraries(east-c-obj PUBLIC mimalloc-static)
        target_compile_definitions(east-c-obj PUBLIC EAST_USE_MIMALLOC=1)
    endif()
endif()

# Compliance test runner (run via: make test-east-c). Built when cross-compiling
# too, so the suite can run against the Windows binary; on Windows it needs
# winpthreads for clock_gettime (east-c alone doesn't pull it in).
add_executable(test_compliance tests/test_compliance.c)
target_link_libraries(test_compliance east-c)
if(WIN32)
    find_package(Threads REQUIRED)
    target_link_libraries(test_compliance Threads::Threads)
endif()

# B-tree leverage gate: proves tidwall/btree.c integrates with east-c's
# allocator, east_value_compare, and refcounted-value teardown before the
# Set/Dict representation is migrated onto it. Not part of the IR compliance
# suite — run directly (and under ASan) to validate the integration.
if(TARGET btreec)
    add_executable(test_btree_gate tests/test_btree_gate.c)
    target_link_libraries(test_btree_gate east-c btreec)
endif()

# Recursive-type lifecycle gate: proves EAST_TYPE_RECURSIVE wrappers follow
# the arena-immortal ownership model — build + intern + type_of_type and
# beast2 type-table decode of a self-referential type, then registry clear.
# Run under ASan/LSan (run_leak_check.sh's build-asan config): a leak or a
# free() of an arena-interior pointer fails the gate.
add_executable(test_recursive_type_lifecycle tests/test_recursive_type_lifecycle.c)
target_link_libraries(test_recursive_type_lifecycle east-c)
add_test(NAME recursive_type_lifecycle COMMAND test_recursive_type_lifecycle)

# Hashmap churn gate: insert/delete past the table's capacity used to fill it
# with tombstones and spin find_index forever, because only live entries counted
# toward the load factor. The failure mode is a hang, not an assertion, so this
# one carries a timeout.
add_executable(test_hashmap_churn tests/test_hashmap_churn.c)
target_link_libraries(test_hashmap_churn east-c)
add_test(NAME hashmap_churn COMMAND test_hashmap_churn)
set_tests_properties(hashmap_churn PROPERTIES TIMEOUT 60)

# type_of_type interning gate (issue #83): east_type_to_value must memoize the
# type->value conversion so two struct fields sharing one interned EastType*
# share one EastValue* sub-tree — otherwise the beast2 type-of-type encoding
# bloats (302 bytes vs the TS-canonical 293). Mirrors the TS
# toEastTypeValueCache. Run under ASan/LSan to validate the memo refcounting.
add_executable(test_type_to_value_interning tests/test_type_to_value_interning.c)
target_link_libraries(test_type_to_value_interning east-c)
add_test(NAME type_to_value_interning COMMAND test_type_to_value_interning)

# Value representation gate (issue #423): a node is sized by its kind, short
# strings live inside it, `none` is shared per VariantType, and struct field
# names are borrowed from the type. Asserts the size classes and the per-shape
# slab footprint. Run under ASan/LSan (run_leak_check.sh's build-asan config)
# for the shared-none cache and borrowed-name lifetimes.
add_executable(test_value_layout tests/test_value_layout.c)
target_link_libraries(test_value_layout east-c)
add_test(NAME value_layout COMMAND test_value_layout)

# GC pacing + pure-data untracking gate (issue #437): full collections must be
# paced on old-generation growth (a fixed interval is quadratic while building
# a large live structure — CPython bpo-4074), and containers whose type cannot
# participate in a cycle (no Function/Ref reachable) must never be tracked.
# Also asserts a real ref cycle is still collected. Run under ASan/LSan
# (run_leak_check.sh's build-asan config): unsound untracking shows as a leak.
add_executable(test_gc_pacing tests/test_gc_pacing.c)
target_link_libraries(test_gc_pacing east-c)
add_test(NAME gc_pacing COMMAND test_gc_pacing)

# Frame-pool gate (issue #763): the v5 writer deflates frames on worker
# threads and appends them in order. A pooled paged encode must be
# byte-identical to the serial algorithm — including its segmentation, which
# is refined from the bytes emitted and so must be decided through the
# writer's bounds — at byte targets that pin the element cap and targets that
# keep the refinement live; in-flight frames must stay within the ring bound;
# and an abandoned writer must free its workers' buffers. Run under ASan/LSan
# (run_leak_check.sh) for the pool's lifetimes.
add_executable(test_beast2_frame_pool tests/test_beast2_frame_pool.c)
target_link_libraries(test_beast2_frame_pool east-c)
add_test(NAME beast2_frame_pool COMMAND test_beast2_frame_pool)
set_tests_properties(beast2_frame_pool PROPERTIES TIMEOUT 300)

# CPU-count gate (#763): worker pools are sized like Node's
# os.availableParallelism() — the affinity mask, capped by the cgroup v2 CPU
# quota. Pins the quota reader against fabricated cgroup trees (nested limits,
# sub-CPU quotas, "max", v1, malformed files) and, on Linux, that a process
# pinned to one CPU counts one.
add_executable(test_cpu_count tests/test_cpu_count.c)
target_link_libraries(test_cpu_count east-c)
add_test(NAME cpu_count COMMAND test_cpu_count)

# Deterministic-deflate equivalence gate (issue #762): the beast2 v5 encoder's
# bytes ARE the wire format (e3 content-addresses them), so the word-packed
# rewrite is held to a copy of the bit-at-a-time original kept in the test,
# over degenerate lengths, byte runs, window-edge matches and bulk row-shaped
# and random input. Throughput is printed, never asserted.
add_executable(test_deflate_equivalence tests/test_deflate_equivalence.c)
target_link_libraries(test_deflate_equivalence east-c)
add_test(NAME deflate_equivalence COMMAND test_deflate_equivalence)

# Beast2 decoder hardening gate (issue #34): truncation/corruption sweeps,
# crafted out-of-bounds type tables, and allocation-overflow guards. The
# beast2 decoders are an untrusted-input boundary — run under ASan
# (run_leak_check.sh's build-asan config) for the full OOB oracle.
add_executable(test_beast2_hardening tests/test_beast2_hardening.c)
target_link_libraries(test_beast2_hardening east-c)
add_test(NAME beast2_hardening COMMAND test_beast2_hardening)

# Beast2 v5 column-projection gate (issue #599): plan validation (subset by
# name, Dict-key/Set-element/variant-case/function-skip refusals with errors
# naming the field), projected vs whole segment equality, skipped-definition
# REF alias detection, open-time pager projection + keyed reads, and the
# sequential reader's projection. Run under ASan/LSan for the skip walker
# and narrow-construction allocation paths.
add_executable(test_beast2_projection tests/test_beast2_projection.c)
target_link_libraries(test_beast2_projection east-c)
add_test(NAME beast2_projection COMMAND test_beast2_projection)

# Nested-loop projection gate: the paged for-loop inference follows an
# inner loop into a nested Array<Struct> or a Dict's values, narrowing the
# items to the fields the inner body reads (a sized array's items to an
# empty struct), while an item used whole keeps only its subtree whole, the
# row used whole still declines, and a shadowing binder declines. A
# projected decode of a small paged blob must agree with the whole decode
# on every field read. Run under ASan/LSan for the mask and plan lifetimes.
add_executable(test_projection_nested tests/test_projection_nested.c)
target_link_libraries(test_projection_nested east-c)
add_test(NAME projection_nested COMMAND test_projection_nested)

# Lazy paged collection value gate (issue #505): EAST_VAL_PAGED construction,
# pager-served size/keyed reads, hydrate-on-demand equivalence with the eager
# decode, and release of every lifetime state. Run under ASan/LSan
# (run_leak_check.sh's build-asan config): the wrapper owns the pager, the
# blob bytes and the hydrated child — the lifetimes this gate exists to feed
# the sanitizer.
add_executable(test_paged_value tests/test_paged_value.c)
target_link_libraries(test_paged_value east-c)
add_test(NAME paged_value COMMAND test_paged_value)

# IR normalization gate (#627): east_ir_normalize is the round-trip equality
# contract — loc_ids stripped, variables/labels renamed in the TypeScript
# lowering's order, captures recomputed, recursive type ids renumbered. Pins
# that a TypeScript-shaped program normalizes to itself, that a python-shaped
# twin (its own names, unlisted captures) normalizes to the same value, and
# the first-difference path east_value_diff_path reports. Run under ASan/LSan
# for the rebuild-every-node allocation paths.
add_executable(test_ir_normalize tests/test_ir_normalize.c)
target_link_libraries(test_ir_normalize east-c)
add_test(NAME ir_normalize COMMAND test_ir_normalize)

# Source-map lifetime gate (#626): a closure — created while a map was current,
# or decoded from a blob's source-map section — holds its own reference to the
# map it resolves loc_ids against, so an error raised through it after the
# compile/decode that produced the map is torn down still resolves. Run under
# ASan/LSan (run_leak_check.sh's build-asan config): the retain/release balance
# across compile, v4/v5 encode, decode and free is the leak oracle.
add_executable(test_source_map_lifetime tests/test_source_map_lifetime.c)
target_link_libraries(test_source_map_lifetime east-c)
add_test(NAME source_map_lifetime COMMAND test_source_map_lifetime)

# Scope + datetime gate (#675, #676): two east-c defects that produced a wrong
# ANSWER rather than an error, and where the TypeScript runner was already
# right — so the same IR gave different results per runtime. A block must
# SCOPE what it binds (two closures over same-named block constants read their
# own), and the epoch-millisecond split must FLOOR (every pre-1970 datetime
# decomposed one second late). Run under ASan/LSan: the block's environment is
# allocated per evaluation and outlives the block only through a closure.
add_executable(test_scope_and_datetime tests/test_scope_and_datetime.c)
target_link_libraries(test_scope_and_datetime east-c)
add_test(NAME scope_and_datetime COMMAND test_scope_and_datetime)

# Scope resolution gate: variables read their frame cell by index once the
# resolver has annotated the tree, and the answer must be what the by-name
# walk gives — a use before its Let seeing the outer binding, shadowing in
# nested blocks, a closure per loop iteration, an Assign visible through a
# closure both ways, match and catch binders, captures bound from outside by
# name, a scoped frame receiving a by-name set, and a function compiled from
# a bare body. Every case runs with resolution on and off
# (EAST_C_NO_SLOT_RESOLVE). Run under ASan/LSan: frames retain their scope
# and the scope outlives the IR through a closure.
add_executable(test_scope_resolution tests/test_scope_resolution.c)
target_link_libraries(test_scope_resolution east-c)
add_test(NAME scope_resolution COMMAND test_scope_resolution)

# Field-read inline cache gate: a GetField node remembers the struct type it
# last read and the field's index, so a value that borrows its names from
# that type is read by index. A coerced struct carrying its own field order
# must never hit the cache (its layout disagrees with the type's), and a
# value stamped with a recursive wrapper must. Run under ASan/LSan for the
# type and value lifetimes it exercises.
add_executable(test_field_cache tests/test_field_cache.c)
target_link_libraries(test_field_cache east-c)
add_test(NAME field_cache COMMAND test_field_cache)

# Value ordering gate: east_value_compare orders Set elements and Dict keys,
# and its order is the canonical wire order every runtime pins. The struct
# case skips the field-name compare when both values borrow their names from
# one interned type; this gate pins that borrowed, copied and mixed-name
# structs still order exactly as a name-then-value compare, that kinds still
# rank in the documented order, and that east_dict_find answers as
# has-then-get did.
add_executable(test_value_compare tests/test_value_compare.c)
target_link_libraries(test_value_compare east-c)
add_test(NAME value_compare COMMAND test_value_compare)

# Platform-signature check parity gate (#62): compiles TS-exported IR against
# drifted typed platform registrations and asserts east_compile_checked's
# error text is byte-identical to the TS analyzer's. Skips (exit 77) unless
# `make test-export` has produced /tmp/east-test-ir/platform_check.
add_executable(test_platform_check tests/test_platform_check.c)
target_link_libraries(test_platform_check east-c)
add_test(NAME platform_check COMMAND test_platform_check)
set_tests_properties(platform_check PROPERTIES SKIP_RETURN_CODE 77)

# Beast2 encode/decode timing harnesses (dev profiling — build Release for real
# numbers). profile_collections builds large Dict/Set values in C and times
# encode + decode per call; profile_beast2_decode times a given .beast2 file.
add_executable(profile_collections scripts/profile_collections.c)
target_link_libraries(profile_collections east-c)
add_executable(profile_beast2_decode scripts/profile_beast2_decode.c)
target_link_libraries(profile_beast2_decode east-c)

# Live-footprint profiler (issue #423): bytes per element for the value shapes
# that issue measured, from both the slab's own accounting and process RSS.
add_executable(profile_value_memory scripts/profile_value_memory.c)
target_link_libraries(profile_value_memory east-c)

# beast2 v4-vs-v5 benchmark (issue #416/#417). Reads the shared corpus written
# by libs/east/contrib/beast2-bench/generate-corpus.ts and emits JSON; see that
# directory's README.md for the three-runtime pipeline. Build Release for real
# numbers.
add_executable(bench_beast2_versions scripts/bench_beast2_versions.c)
target_link_libraries(bench_beast2_versions east-c)
