From 4216f43ed04ebf4ca44ef553fa92d3d24f0fecbf Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 17 Aug 2026 13:15:11 +0200 Subject: [PATCH 1/7] meson: use c2x so the project configures on meson 1.3 meson only added 'c23' as a recognised c_std value in 1.4, but the project declares meson_version >=1.3.0. On meson 1.3.x configuration fails outright: ERROR: Unknown C std ['c23']. Possible values are [... 'c2x' ...] Newer meson still accepts 'c2x' and it selects the same standard, so 'c2x' is the spelling that works across the supported meson range. --- meson.build | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index dca35a7..6407cfd 100644 --- a/meson.build +++ b/meson.build @@ -3,7 +3,10 @@ project( 'c', version: '0.1.0', default_options: [ - 'c_std=c23', + # c2x, not c23: meson only learned 'c23' in 1.4, and this project declares + # meson_version >=1.3.0. Newer meson still accepts c2x, and both select the + # same standard, so c2x is the portable spelling. + 'c_std=c2x', 'warning_level=3', 'werror=true', 'b_lundef=false', From bbba21edf94711cd0e095d618f6c41af9a1fd900 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 17 Aug 2026 13:15:34 +0200 Subject: [PATCH 2/7] memgraph: build against headers without the mgp_*_move API sync_module.c calls mgp_list_append_move, mgp_map_insert_move and mgp_unordered_map_make_empty. Memgraph 3.1.1 ships none of them, so the module fails to compile against a stock 3.1 install. Add guarded shims implementing the three on top of the copying variants, and detect the situation in meson via cc.has_header_symbol rather than requiring -DSYNC_KGRAPH_MGP_COMPAT to be passed by hand. Against newer headers the shims compile out entirely and nothing changes. Note for review: mgp_list_append and mgp_map_insert copy the value, so the shims destroy the source on success to preserve the move-semantics contract the call sites rely on (they destroy the value themselves on failure). That mirrors what the real *_move functions do, but it is an assumption about the intended contract rather than something the headers state, so it is worth confirming. --- meson.build | 10 ++++++++++ src/memgraph/sync_module.c | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/meson.build b/meson.build index 6407cfd..2d786f8 100644 --- a/meson.build +++ b/meson.build @@ -99,6 +99,16 @@ if memgraph_feature.enabled() and not has_memgraph endif if has_memgraph and not memgraph_feature.disabled() + # Memgraph gained the mgp_*_move API (and mgp_unordered_map_make_empty) after + # 3.1. Against older headers the module needs the compat shims in + # src/memgraph/sync_module.c, so detect the API rather than asking the builder + # to pass -DSYNC_KGRAPH_MGP_COMPAT by hand. + if not cc.has_header_symbol('mg_procedure.h', 'mgp_list_append_move', + args: memgraph_args) + message('Memgraph headers predate mgp_*_move; enabling compat shims.') + memgraph_args += ['-DSYNC_KGRAPH_MGP_COMPAT'] + endif + module_link_args = [] if host_machine.system() == 'darwin' module_link_args += ['-Wl,-undefined,dynamic_lookup'] diff --git a/src/memgraph/sync_module.c b/src/memgraph/sync_module.c index 694dfed..4a043c3 100644 --- a/src/memgraph/sync_module.c +++ b/src/memgraph/sync_module.c @@ -16,6 +16,35 @@ static bool mg_ok(enum mgp_error error) { return error == MGP_ERROR_NO_ERROR; } +#ifdef SYNC_KGRAPH_MGP_COMPAT +// Compat for Memgraph headers that predate the *_move API and +// mgp_unordered_map_make_empty. The non-move variants copy the value, so +// these shims destroy the source on success to preserve the move-semantics +// contract at the call sites (call sites destroy the value themselves on +// failure). +static enum mgp_error sync_compat_list_append_move(struct mgp_list *list, + struct mgp_value *value) { + const enum mgp_error error = mgp_list_append(list, value); + if (error == MGP_ERROR_NO_ERROR) { + mgp_value_destroy(value); + } + return error; +} + +static enum mgp_error sync_compat_map_insert_move(struct mgp_map *map, const char *key, + struct mgp_value *value) { + const enum mgp_error error = mgp_map_insert(map, key, value); + if (error == MGP_ERROR_NO_ERROR) { + mgp_value_destroy(value); + } + return error; +} + +#define mgp_list_append_move sync_compat_list_append_move +#define mgp_map_insert_move sync_compat_map_insert_move +#define mgp_unordered_map_make_empty mgp_map_make_empty +#endif + static void set_error(struct mgp_result *result, const char *message) { (void)mgp_result_set_error_msg(result, message); } From 6d9bd6885e635e1642f43293932ab2fcbbc8493a Mon Sep 17 00:00:00 2001 From: gaperez64 Date: Mon, 17 Aug 2026 20:03:23 +0200 Subject: [PATCH 3/7] Fix Memgraph compatibility formatting --- src/memgraph/sync_module.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/memgraph/sync_module.c b/src/memgraph/sync_module.c index 4a043c3..040b31f 100644 --- a/src/memgraph/sync_module.c +++ b/src/memgraph/sync_module.c @@ -22,8 +22,7 @@ static bool mg_ok(enum mgp_error error) { // these shims destroy the source on success to preserve the move-semantics // contract at the call sites (call sites destroy the value themselves on // failure). -static enum mgp_error sync_compat_list_append_move(struct mgp_list *list, - struct mgp_value *value) { +static enum mgp_error sync_compat_list_append_move(struct mgp_list *list, struct mgp_value *value) { const enum mgp_error error = mgp_list_append(list, value); if (error == MGP_ERROR_NO_ERROR) { mgp_value_destroy(value); From ed31f42d559324a518c5ba3e6ab8e8228b4b3d26 Mon Sep 17 00:00:00 2001 From: gaperez64 Date: Mon, 17 Aug 2026 20:24:21 +0200 Subject: [PATCH 4/7] Implement synchronize-or-reveal core --- .clang-tidy | 2 + include/sync_kgraph/sync.h | 202 ++-- meson.build | 8 +- scripts/check-format.sh | 3 + scripts/coverage.sh | 6 +- scripts/run-clang-tidy.sh | 4 +- src/oracle.c | 523 ++++++++++ src/planner.c | 1177 ++++++++++++++++++++++ src/sync.c | 1936 +++++++----------------------------- src/sync_cli.c | 209 ++-- src/sync_internal.h | 43 + tests/test_core.c | 557 ++++++----- 12 files changed, 2713 insertions(+), 1957 deletions(-) create mode 100644 src/oracle.c create mode 100644 src/planner.c create mode 100644 src/sync_internal.h diff --git a/.clang-tidy b/.clang-tidy index 91d87f7..29cbc45 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,6 @@ Checks: > clang-analyzer-*, + -clang-analyzer-security.ArrayBound, -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, bugprone-*, -bugprone-multi-level-implicit-pointer-conversion, @@ -9,6 +10,7 @@ Checks: > concurrency-*, -concurrency-mt-unsafe, misc-*, + -misc-include-cleaner, performance-*, portability-*, readability-*, diff --git a/include/sync_kgraph/sync.h b/include/sync_kgraph/sync.h index 7ab4f7a..b0dd488 100644 --- a/include/sync_kgraph/sync.h +++ b/include/sync_kgraph/sync.h @@ -3,11 +3,14 @@ #include #include +#include #ifdef __cplusplus extern "C" { #endif +#define SG_INDEX_NONE SIZE_MAX + typedef enum { SG_OK = 0, SG_ERR_ALLOC, @@ -16,105 +19,162 @@ typedef enum { SG_ERR_NOT_FOUND, SG_ERR_INCOMPLETE, SG_ERR_NONDETERMINISTIC, - SG_ERR_UNSYNCHRONIZABLE, + SG_ERR_INVALID_MODEL, SG_ERR_RESOURCE_BOUND, + SG_ERR_STALE_GENERATION, } sg_status; typedef enum { - SG_MODE_SYNC = 0, - SG_MODE_REACH = 1, - SG_MODE_REACH_AND_SYNC = 2, -} sg_mode; + SG_OUTCOME_PLAN = 0, + SG_OUTCOME_ALREADY_SATISFIED, + SG_OUTCOME_NO_PLAN, + SG_OUTCOME_RESOURCE_BOUND, +} sg_plan_outcome; + +typedef enum { + SG_METHOD_NONE = 0, + SG_METHOD_PAIR_MERGE, + SG_METHOD_PAIR_RESOLUTION, + SG_METHOD_PARTITION_BFS, +} sg_plan_method; typedef enum { - SG_RESULT_TRIVIAL = 0, - SG_RESULT_PAIR_GREEDY, - SG_RESULT_PAIR_GREEDY_TARGETED, - SG_RESULT_EXACT_EXPANDED, - SG_RESULT_RESOURCE_BOUND, - SG_RESULT_FAILURE, -} sg_result_kind; - -typedef struct sg_dfa sg_dfa; -typedef struct sg_dfa_builder sg_dfa_builder; + SG_MONITOR_CONTINUE = 0, + SG_MONITOR_REPLAN, + SG_MONITOR_MODEL_VIOLATION, + SG_MONITOR_STALE_GENERATION, + SG_MONITOR_WAIT, +} sg_monitor_decision; + +typedef struct sg_automaton sg_automaton; +typedef struct sg_automaton_builder sg_automaton_builder; typedef struct sg_pair_oracle sg_pair_oracle; typedef struct { - size_t *letters; + size_t *actions; size_t length; size_t capacity; } sg_word; typedef struct { - sg_result_kind kind; - sg_status status; + sg_plan_outcome outcome; + sg_plan_method method; sg_word word; size_t final_state; - size_t final_count; -} sg_word_result; + size_t final_support_size; + size_t best_support_size; + size_t worst_support_size; + size_t branch_count; + size_t expansions; + bool homing; + uint64_t generation; + uint64_t planning_time_us; +} sg_plan_result; -typedef sg_status (*sg_cache_visitor)(void *ctx, const size_t *states, size_t state_count, - const sg_word *word); +typedef struct { + size_t pair; + bool mergeable; + size_t merge_distance; + size_t merge_action; + size_t merge_next_pair; + bool resolvable; + size_t resolution_distance; + size_t resolution_action; + size_t resolution_next_pair; +} sg_pair_record; + +typedef struct { + sg_monitor_decision decision; + size_t *expected_states; + size_t expected_count; + size_t *unexpected_states; + size_t unexpected_count; + uint64_t generation; +} sg_monitor_result; + +typedef sg_status (*sg_explain_visitor)(void *context, size_t step, size_t action, + const size_t *predicted_states, size_t predicted_count, + const size_t *output_trace, size_t trace_length, + const size_t *branch_states, size_t branch_count); const char *sg_status_name(sg_status status); -const char *sg_result_kind_name(sg_result_kind kind); -const char *sg_mode_name(sg_mode mode); -sg_status sg_mode_parse(const char *name, sg_mode *mode); +const char *sg_plan_outcome_name(sg_plan_outcome outcome); +const char *sg_plan_method_name(sg_plan_method method); +const char *sg_monitor_decision_name(sg_monitor_decision decision); sg_status sg_word_init(sg_word *word); void sg_word_free(sg_word *word); -sg_status sg_word_append(sg_word *word, size_t letter); -sg_status sg_word_prepend(sg_word *word, size_t letter); - -sg_status sg_dfa_builder_init(sg_dfa_builder **builder); -void sg_dfa_builder_free(sg_dfa_builder *builder); -sg_status sg_dfa_builder_add_state(sg_dfa_builder *builder, const char *state_key); -sg_status sg_dfa_builder_add_letter(sg_dfa_builder *builder, const char *letter); -sg_status sg_dfa_builder_add_transition(sg_dfa_builder *builder, const char *source_key, - const char *letter, const char *target_key); -sg_status sg_dfa_builder_build(sg_dfa_builder *builder, bool complete_with_sink, sg_dfa **dfa); - -void sg_dfa_free(sg_dfa *dfa); -size_t sg_dfa_state_count(const sg_dfa *dfa); -size_t sg_dfa_letter_count(const sg_dfa *dfa); -size_t sg_dfa_transition_count(const sg_dfa *dfa); -const char *sg_dfa_state_key(const sg_dfa *dfa, size_t state); -const char *sg_dfa_letter_key(const sg_dfa *dfa, size_t letter); -size_t sg_dfa_transition(const sg_dfa *dfa, size_t state, size_t letter); -sg_status sg_dfa_find_state(const sg_dfa *dfa, const char *state_key, size_t *state); -sg_status sg_dfa_find_letter(const sg_dfa *dfa, const char *letter, size_t *letter_id); - -sg_status sg_pair_oracle_build(const sg_dfa *dfa, sg_pair_oracle **oracle); +sg_status sg_word_append(sg_word *word, size_t action); + +sg_status sg_automaton_builder_init(sg_automaton_builder **builder); +void sg_automaton_builder_free(sg_automaton_builder *builder); +sg_status sg_automaton_builder_add_state(sg_automaton_builder *builder, const char *state_key); +sg_status sg_automaton_builder_add_action(sg_automaton_builder *builder, const char *action_key); +sg_status sg_automaton_builder_add_output(sg_automaton_builder *builder, const char *output_key); +sg_status sg_automaton_builder_add_transition(sg_automaton_builder *builder, const char *source_key, + const char *action_key, const char *target_key); +sg_status sg_automaton_builder_add_observation(sg_automaton_builder *builder, + const char *source_key, const char *action_key, + const char *output_key); +sg_status sg_automaton_builder_build(sg_automaton_builder *builder, uint64_t generation, + sg_automaton **automaton); + +void sg_automaton_free(sg_automaton *automaton); +uint64_t sg_automaton_generation(const sg_automaton *automaton); +size_t sg_automaton_state_count(const sg_automaton *automaton); +size_t sg_automaton_action_count(const sg_automaton *automaton); +size_t sg_automaton_output_count(const sg_automaton *automaton); +size_t sg_automaton_transition_count(const sg_automaton *automaton); +const char *sg_automaton_state_key(const sg_automaton *automaton, size_t state); +const char *sg_automaton_action_key(const sg_automaton *automaton, size_t action); +const char *sg_automaton_output_key(const sg_automaton *automaton, size_t output); +size_t sg_automaton_transition(const sg_automaton *automaton, size_t state, size_t action); +size_t sg_automaton_observation(const sg_automaton *automaton, size_t state, size_t action); +sg_status sg_automaton_find_state(const sg_automaton *automaton, const char *state_key, + size_t *state); +sg_status sg_automaton_find_action(const sg_automaton *automaton, const char *action_key, + size_t *action); +sg_status sg_automaton_find_output(const sg_automaton *automaton, const char *output_key, + size_t *output); + +sg_status sg_pair_oracle_build(const sg_automaton *automaton, sg_pair_oracle **oracle); +sg_status sg_pair_oracle_restore(const sg_automaton *automaton, const sg_pair_record *records, + size_t record_count, sg_pair_oracle **oracle); void sg_pair_oracle_free(sg_pair_oracle *oracle); size_t sg_pair_oracle_pair_count(const sg_pair_oracle *oracle); size_t sg_pair_oracle_pair_edge_count(const sg_pair_oracle *oracle); size_t sg_pair_oracle_mergeable_pair_count(const sg_pair_oracle *oracle); +size_t sg_pair_oracle_resolvable_pair_count(const sg_pair_oracle *oracle); sg_status sg_pair_oracle_pair_states(const sg_pair_oracle *oracle, size_t pair, size_t *first, size_t *second); -sg_status sg_pair_oracle_pair_next(const sg_pair_oracle *oracle, size_t pair, size_t letter, - size_t *next_pair); -sg_status sg_pair_oracle_pair_witness(const sg_pair_oracle *oracle, size_t pair, bool *has_witness, - size_t *distance, size_t *letter, size_t *next_pair); -bool sg_pair_oracle_has_witness(const sg_pair_oracle *oracle, size_t first, size_t second); -sg_status sg_pair_oracle_witness_word(const sg_pair_oracle *oracle, size_t first, size_t second, - sg_word *word); - -sg_status sg_word_for_set(const sg_dfa *dfa, const sg_pair_oracle *oracle, const size_t *initial, - size_t initial_count, const size_t *targets, size_t target_count, - sg_mode mode, size_t exact_budget, sg_word_result *result); -void sg_word_result_free(sg_word_result *result); -sg_status sg_expand_cache(const sg_dfa *dfa, const size_t *targets, size_t target_count, - sg_mode mode, size_t budget, size_t *expanded, size_t *cache_size); -sg_status sg_expand_cache_visit(const sg_dfa *dfa, const size_t *targets, size_t target_count, - sg_mode mode, size_t budget, sg_cache_visitor visitor, void *ctx, - size_t *expanded, size_t *cache_size); - -sg_status sg_apply_word_to_set(const sg_dfa *dfa, const size_t *initial, size_t initial_count, - const sg_word *word, size_t *output, size_t *output_count); -sg_status sg_explain_word(const sg_dfa *dfa, const size_t *initial, size_t initial_count, - const sg_word *word, size_t **steps, size_t **step_counts, - size_t *step_count); -void sg_explain_free(size_t *steps, size_t *step_counts); +sg_status sg_pair_oracle_pair_step(const sg_pair_oracle *oracle, size_t pair, size_t action, + size_t *next_pair, bool *outputs_differ); +sg_status sg_pair_oracle_record(const sg_pair_oracle *oracle, size_t pair, sg_pair_record *record); +sg_status sg_pair_oracle_merge_word(const sg_pair_oracle *oracle, size_t first, size_t second, + sg_word *word); +sg_status sg_pair_oracle_resolution_word(const sg_pair_oracle *oracle, size_t first, size_t second, + sg_word *word); + +sg_status sg_plan_sync(const sg_automaton *automaton, const sg_pair_oracle *oracle, + const size_t *initial_states, size_t initial_count, size_t budget, + sg_plan_result *result); +sg_status sg_plan_disambiguate(const sg_automaton *automaton, const sg_pair_oracle *oracle, + const size_t *initial_states, size_t initial_count, size_t bound, + size_t budget, sg_plan_result *result); +void sg_plan_result_free(sg_plan_result *result); + +sg_status sg_apply_word(const sg_automaton *automaton, const size_t *initial_states, + size_t initial_count, const sg_word *word, size_t *output_states, + size_t *output_count); +sg_status sg_explain_plan(const sg_automaton *automaton, uint64_t plan_generation, + const size_t *initial_states, size_t initial_count, const sg_word *word, + sg_explain_visitor visitor, void *context); +sg_status sg_validate_update(const sg_automaton *automaton, uint64_t plan_generation, + const size_t *initial_states, size_t initial_count, + const sg_word *word, size_t completed_steps, + const size_t *reported_states, size_t reported_count, + bool localizer_available, sg_monitor_result *result); +void sg_monitor_result_free(sg_monitor_result *result); #ifdef __cplusplus } diff --git a/meson.build b/meson.build index 2d786f8..ddffeef 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'sync-kgraph', 'c', - version: '0.1.0', + version: '0.2.0', default_options: [ # c2x, not c23: meson only learned 'c23' in 1.4, and this project declares # meson_version >=1.3.0. Newer meson still accepts c2x, and both select the @@ -35,7 +35,11 @@ endforeach inc = include_directories('include') -sync_sources = files('src/sync.c') +sync_sources = files( + 'src/oracle.c', + 'src/planner.c', + 'src/sync.c', +) sync_lib = static_library( 'sync_kgraph', diff --git a/scripts/check-format.sh b/scripts/check-format.sh index e42dedd..d20db27 100644 --- a/scripts/check-format.sh +++ b/scripts/check-format.sh @@ -3,7 +3,10 @@ set -eu clang-format --dry-run --Werror \ include/sync_kgraph/sync.h \ + src/oracle.c \ + src/planner.c \ src/sync.c \ + src/sync_internal.h \ src/sync_cli.c \ src/memgraph/sync_module.c \ tests/test_core.c diff --git a/scripts/coverage.sh b/scripts/coverage.sh index bc72f90..9282273 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -7,8 +7,10 @@ CC="${CC:-clang}" meson setup "$builddir" --wipe -Db_coverage=true -Dmemgraph=di meson test -C "$builddir" --print-errorlogs gcovr \ --root . \ + --object-directory "$builddir" \ --gcov-executable "llvm-cov gcov" \ - --filter src/sync.c \ + --filter 'src/(oracle|planner|sync)\.c' \ --exclude 'tests/.*' \ --fail-under-line 75 \ - --print-summary + --print-summary \ + "$builddir" diff --git a/scripts/run-clang-tidy.sh b/scripts/run-clang-tidy.sh index 3aca1e1..b7adf56 100644 --- a/scripts/run-clang-tidy.sh +++ b/scripts/run-clang-tidy.sh @@ -8,10 +8,10 @@ if [ -n "$memgraph_include_dir" ]; then CC="${CC:-clang}" meson setup "$builddir" --wipe \ -Dmemgraph=enabled \ -Dmemgraph_include_dir="$memgraph_include_dir" - tidy_files="src/sync.c src/sync_cli.c src/memgraph/sync_module.c tests/test_core.c" + tidy_files="src/oracle.c src/planner.c src/sync.c src/sync_cli.c src/memgraph/sync_module.c tests/test_core.c" else CC="${CC:-clang}" meson setup "$builddir" --wipe -Dmemgraph=disabled - tidy_files="src/sync.c src/sync_cli.c tests/test_core.c" + tidy_files="src/oracle.c src/planner.c src/sync.c src/sync_cli.c tests/test_core.c" fi ninja -C "$builddir" diff --git a/src/oracle.c b/src/oracle.c new file mode 100644 index 0000000..1b4b56c --- /dev/null +++ b/src/oracle.c @@ -0,0 +1,523 @@ +#include "sync_internal.h" + +#include +#include + +typedef struct { + size_t *offsets; + size_t *pairs; + size_t *actions; +} sg_predecessors; + +size_t sg_pair_index(size_t state_count, size_t first, size_t second) { + if (first > second) { + const size_t temporary = first; + first = second; + second = temporary; + } + return (first * state_count) - ((first * (first + 1U)) / 2U) + second; +} + +static bool sg_pair_count(size_t state_count, size_t *pair_count) { + if (pair_count == NULL || state_count == SIZE_MAX) { + return false; + } + size_t product = 0U; + if (!sg_size_multiply(state_count, state_count + 1U, &product)) { + return false; + } + *pair_count = product / 2U; + return true; +} + +static void sg_oracle_arrays_initialize(sg_pair_oracle *oracle) { + const size_t pairs = oracle->pair_count; + for (size_t pair = 0U; pair < pairs; ++pair) { + oracle->merge_distance[pair] = SG_INDEX_NONE; + oracle->merge_action[pair] = SG_INDEX_NONE; + oracle->merge_next[pair] = SG_INDEX_NONE; + oracle->resolution_distance[pair] = SG_INDEX_NONE; + oracle->resolution_action[pair] = SG_INDEX_NONE; + oracle->resolution_next[pair] = SG_INDEX_NONE; + } +} + +static sg_status sg_oracle_allocate(const sg_automaton *automaton, sg_pair_oracle **oracle) { + size_t pair_count = 0U; + if (!sg_pair_count(automaton->state_count, &pair_count)) { + return SG_ERR_ALLOC; + } + size_t edge_count = 0U; + if (!sg_size_multiply(pair_count, automaton->action_count, &edge_count)) { + return SG_ERR_ALLOC; + } + sg_pair_oracle *created = calloc(1U, sizeof(*created)); + if (created == NULL) { + return SG_ERR_ALLOC; + } + created->automaton = automaton; + created->pair_count = pair_count; + created->first = calloc(pair_count, sizeof(*created->first)); + created->second = calloc(pair_count, sizeof(*created->second)); + created->next = calloc(edge_count, sizeof(*created->next)); + created->outputs_differ = calloc(edge_count, sizeof(*created->outputs_differ)); + created->merge_distance = calloc(pair_count, sizeof(*created->merge_distance)); + created->merge_action = calloc(pair_count, sizeof(*created->merge_action)); + created->merge_next = calloc(pair_count, sizeof(*created->merge_next)); + created->resolution_distance = calloc(pair_count, sizeof(*created->resolution_distance)); + created->resolution_action = calloc(pair_count, sizeof(*created->resolution_action)); + created->resolution_next = calloc(pair_count, sizeof(*created->resolution_next)); + if (created->first == NULL || created->second == NULL || created->next == NULL || + created->outputs_differ == NULL || created->merge_distance == NULL || + created->merge_action == NULL || created->merge_next == NULL || + created->resolution_distance == NULL || created->resolution_action == NULL || + created->resolution_next == NULL) { + sg_pair_oracle_free(created); + return SG_ERR_ALLOC; + } + sg_oracle_arrays_initialize(created); + *oracle = created; + return SG_OK; +} + +static void sg_oracle_fill_pairs(sg_pair_oracle *oracle) { + const size_t states = oracle->automaton->state_count; + const size_t actions = oracle->automaton->action_count; + size_t pair = 0U; + for (size_t first = 0U; first < states; ++first) { + for (size_t second = first; second < states; ++second) { + oracle->first[pair] = first; + oracle->second[pair] = second; + for (size_t action = 0U; action < actions; ++action) { + const size_t first_next = sg_automaton_transition(oracle->automaton, first, action); + const size_t second_next = sg_automaton_transition(oracle->automaton, second, action); + const size_t edge = (pair * actions) + action; + oracle->next[edge] = sg_pair_index(states, first_next, second_next); + oracle->outputs_differ[edge] = sg_automaton_observation(oracle->automaton, first, action) != + sg_automaton_observation(oracle->automaton, second, action); + } + ++pair; + } + } +} + +static void sg_predecessors_free(sg_predecessors *predecessors) { + if (predecessors == NULL) { + return; + } + free(predecessors->offsets); + free(predecessors->pairs); + free(predecessors->actions); + predecessors->offsets = NULL; + predecessors->pairs = NULL; + predecessors->actions = NULL; +} + +static sg_status sg_predecessors_build(const sg_pair_oracle *oracle, + sg_predecessors *predecessors) { + const size_t actions = oracle->automaton->action_count; + const size_t edge_count = oracle->pair_count * actions; + size_t *counts = calloc(oracle->pair_count, sizeof(*counts)); + predecessors->offsets = calloc(oracle->pair_count + 1U, sizeof(*predecessors->offsets)); + predecessors->pairs = calloc(edge_count, sizeof(*predecessors->pairs)); + predecessors->actions = calloc(edge_count, sizeof(*predecessors->actions)); + if (counts == NULL || predecessors->offsets == NULL || predecessors->pairs == NULL || + predecessors->actions == NULL) { + free(counts); + sg_predecessors_free(predecessors); + return SG_ERR_ALLOC; + } + for (size_t edge = 0U; edge < edge_count; ++edge) { + ++counts[oracle->next[edge]]; + } + for (size_t pair = 0U; pair < oracle->pair_count; ++pair) { + predecessors->offsets[pair + 1U] = predecessors->offsets[pair] + counts[pair]; + counts[pair] = predecessors->offsets[pair]; + } + for (size_t pair = 0U; pair < oracle->pair_count; ++pair) { + for (size_t action = 0U; action < actions; ++action) { + const size_t target = oracle->next[(pair * actions) + action]; + const size_t position = counts[target]; + predecessors->pairs[position] = pair; + predecessors->actions[position] = action; + ++counts[target]; + } + } + free(counts); + return SG_OK; +} + +static sg_status sg_merge_distances(sg_pair_oracle *oracle, const sg_predecessors *predecessors) { + size_t *queue = calloc(oracle->pair_count, sizeof(*queue)); + if (queue == NULL) { + return SG_ERR_ALLOC; + } + size_t head = 0U; + size_t tail = 0U; + for (size_t pair = 0U; pair < oracle->pair_count; ++pair) { + if (oracle->first[pair] == oracle->second[pair]) { + oracle->merge_distance[pair] = 0U; + queue[tail] = pair; + ++tail; + } + } + while (head < tail) { + const size_t target = queue[head]; + ++head; + for (size_t index = predecessors->offsets[target]; index < predecessors->offsets[target + 1U]; + ++index) { + const size_t source = predecessors->pairs[index]; + if (oracle->merge_distance[source] == SG_INDEX_NONE) { + oracle->merge_distance[source] = oracle->merge_distance[target] + 1U; + queue[tail] = source; + ++tail; + } + } + } + free(queue); + return SG_OK; +} + +static sg_status sg_resolution_distances(sg_pair_oracle *oracle, + const sg_predecessors *predecessors) { + const size_t actions = oracle->automaton->action_count; + size_t *queue = calloc(oracle->pair_count, sizeof(*queue)); + if (queue == NULL) { + return SG_ERR_ALLOC; + } + size_t head = 0U; + size_t tail = 0U; + for (size_t pair = 0U; pair < oracle->pair_count; ++pair) { + if (oracle->first[pair] == oracle->second[pair]) { + oracle->resolution_distance[pair] = 0U; + queue[tail] = pair; + ++tail; + } + } + for (size_t pair = 0U; pair < oracle->pair_count; ++pair) { + if (oracle->resolution_distance[pair] != SG_INDEX_NONE) { + continue; + } + for (size_t action = 0U; action < actions; ++action) { + if (oracle->outputs_differ[(pair * actions) + action]) { + oracle->resolution_distance[pair] = 1U; + queue[tail] = pair; + ++tail; + break; + } + } + } + while (head < tail) { + const size_t target = queue[head]; + ++head; + for (size_t index = predecessors->offsets[target]; index < predecessors->offsets[target + 1U]; + ++index) { + const size_t source = predecessors->pairs[index]; + const size_t action = predecessors->actions[index]; + if (!oracle->outputs_differ[(source * actions) + action] && + oracle->resolution_distance[source] == SG_INDEX_NONE) { + oracle->resolution_distance[source] = oracle->resolution_distance[target] + 1U; + queue[tail] = source; + ++tail; + } + } + } + free(queue); + return SG_OK; +} + +static void sg_choose_witnesses(sg_pair_oracle *oracle) { + const size_t actions = oracle->automaton->action_count; + for (size_t pair = 0U; pair < oracle->pair_count; ++pair) { + if (oracle->merge_distance[pair] != SG_INDEX_NONE && oracle->merge_distance[pair] != 0U) { + for (size_t action = 0U; action < actions; ++action) { + const size_t next = oracle->next[(pair * actions) + action]; + if (oracle->merge_distance[next] != SG_INDEX_NONE && + oracle->merge_distance[next] + 1U == oracle->merge_distance[pair]) { + oracle->merge_action[pair] = action; + oracle->merge_next[pair] = next; + break; + } + } + } + if (oracle->resolution_distance[pair] == SG_INDEX_NONE || + oracle->resolution_distance[pair] == 0U) { + continue; + } + for (size_t action = 0U; action < actions; ++action) { + const size_t edge = (pair * actions) + action; + if (oracle->resolution_distance[pair] == 1U && oracle->outputs_differ[edge]) { + oracle->resolution_action[pair] = action; + oracle->resolution_next[pair] = SG_INDEX_NONE; + break; + } + const size_t next = oracle->next[edge]; + if (!oracle->outputs_differ[edge] && oracle->resolution_distance[next] != SG_INDEX_NONE && + oracle->resolution_distance[next] + 1U == oracle->resolution_distance[pair]) { + oracle->resolution_action[pair] = action; + oracle->resolution_next[pair] = next; + break; + } + } + } +} + +sg_status sg_pair_oracle_build(const sg_automaton *automaton, sg_pair_oracle **oracle) { + if (automaton == NULL || oracle == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + *oracle = NULL; + sg_pair_oracle *created = NULL; + sg_status status = sg_oracle_allocate(automaton, &created); + if (status != SG_OK) { + return status; + } + sg_oracle_fill_pairs(created); + sg_predecessors predecessors = {0}; + status = sg_predecessors_build(created, &predecessors); + if (status == SG_OK) { + status = sg_merge_distances(created, &predecessors); + } + if (status == SG_OK) { + status = sg_resolution_distances(created, &predecessors); + } + sg_predecessors_free(&predecessors); + if (status != SG_OK) { + sg_pair_oracle_free(created); + return status; + } + sg_choose_witnesses(created); + *oracle = created; + return SG_OK; +} + +void sg_pair_oracle_free(sg_pair_oracle *oracle) { + if (oracle == NULL) { + return; + } + free(oracle->first); + free(oracle->second); + free(oracle->next); + free(oracle->outputs_differ); + free(oracle->merge_distance); + free(oracle->merge_action); + free(oracle->merge_next); + free(oracle->resolution_distance); + free(oracle->resolution_action); + free(oracle->resolution_next); + free(oracle); +} + +size_t sg_pair_oracle_pair_count(const sg_pair_oracle *oracle) { + return oracle == NULL ? 0U : oracle->pair_count; +} + +size_t sg_pair_oracle_pair_edge_count(const sg_pair_oracle *oracle) { + return oracle == NULL ? 0U : oracle->pair_count * oracle->automaton->action_count; +} + +static size_t sg_reachable_pair_count(const sg_pair_oracle *oracle, const size_t *distances) { + if (oracle == NULL) { + return 0U; + } + size_t count = 0U; + for (size_t pair = 0U; pair < oracle->pair_count; ++pair) { + if (distances[pair] != SG_INDEX_NONE) { + ++count; + } + } + return count; +} + +size_t sg_pair_oracle_mergeable_pair_count(const sg_pair_oracle *oracle) { + return oracle == NULL ? 0U : sg_reachable_pair_count(oracle, oracle->merge_distance); +} + +size_t sg_pair_oracle_resolvable_pair_count(const sg_pair_oracle *oracle) { + return oracle == NULL ? 0U : sg_reachable_pair_count(oracle, oracle->resolution_distance); +} + +sg_status sg_pair_oracle_pair_states(const sg_pair_oracle *oracle, size_t pair, size_t *first, + size_t *second) { + if (oracle == NULL || first == NULL || second == NULL || pair >= oracle->pair_count) { + return SG_ERR_INVALID_ARGUMENT; + } + *first = oracle->first[pair]; + *second = oracle->second[pair]; + return SG_OK; +} + +sg_status sg_pair_oracle_pair_step(const sg_pair_oracle *oracle, size_t pair, size_t action, + size_t *next_pair, bool *outputs_differ) { + if (oracle == NULL || next_pair == NULL || outputs_differ == NULL || pair >= oracle->pair_count || + action >= oracle->automaton->action_count) { + return SG_ERR_INVALID_ARGUMENT; + } + const size_t edge = (pair * oracle->automaton->action_count) + action; + *next_pair = oracle->next[edge]; + *outputs_differ = oracle->outputs_differ[edge]; + return SG_OK; +} + +sg_status sg_pair_oracle_record(const sg_pair_oracle *oracle, size_t pair, sg_pair_record *record) { + if (oracle == NULL || record == NULL || pair >= oracle->pair_count) { + return SG_ERR_INVALID_ARGUMENT; + } + *record = (sg_pair_record){ + .pair = pair, + .mergeable = oracle->merge_distance[pair] != SG_INDEX_NONE, + .merge_distance = oracle->merge_distance[pair], + .merge_action = oracle->merge_action[pair], + .merge_next_pair = oracle->merge_next[pair], + .resolvable = oracle->resolution_distance[pair] != SG_INDEX_NONE, + .resolution_distance = oracle->resolution_distance[pair], + .resolution_action = oracle->resolution_action[pair], + .resolution_next_pair = oracle->resolution_next[pair], + }; + return SG_OK; +} + +static bool sg_merge_record_valid(const sg_pair_oracle *oracle, size_t pair) { + const size_t distance = oracle->merge_distance[pair]; + if (distance == SG_INDEX_NONE) { + return oracle->merge_action[pair] == SG_INDEX_NONE && oracle->merge_next[pair] == SG_INDEX_NONE; + } + if (distance == 0U) { + return oracle->first[pair] == oracle->second[pair] && + oracle->merge_action[pair] == SG_INDEX_NONE && oracle->merge_next[pair] == SG_INDEX_NONE; + } + const size_t action = oracle->merge_action[pair]; + const size_t next = oracle->merge_next[pair]; + return action < oracle->automaton->action_count && next < oracle->pair_count && + oracle->next[(pair * oracle->automaton->action_count) + action] == next && + oracle->merge_distance[next] != SG_INDEX_NONE && + oracle->merge_distance[next] + 1U == distance; +} + +static bool sg_resolution_record_valid(const sg_pair_oracle *oracle, size_t pair) { + const size_t distance = oracle->resolution_distance[pair]; + if (distance == SG_INDEX_NONE) { + return oracle->resolution_action[pair] == SG_INDEX_NONE && + oracle->resolution_next[pair] == SG_INDEX_NONE; + } + if (distance == 0U) { + return oracle->first[pair] == oracle->second[pair] && + oracle->resolution_action[pair] == SG_INDEX_NONE && + oracle->resolution_next[pair] == SG_INDEX_NONE; + } + const size_t action = oracle->resolution_action[pair]; + if (action >= oracle->automaton->action_count) { + return false; + } + const size_t edge = (pair * oracle->automaton->action_count) + action; + if (oracle->resolution_next[pair] == SG_INDEX_NONE) { + return distance == 1U && oracle->outputs_differ[edge]; + } + const size_t next = oracle->resolution_next[pair]; + return next < oracle->pair_count && !oracle->outputs_differ[edge] && oracle->next[edge] == next && + oracle->resolution_distance[next] != SG_INDEX_NONE && + oracle->resolution_distance[next] + 1U == distance; +} + +sg_status sg_pair_oracle_restore(const sg_automaton *automaton, const sg_pair_record *records, + size_t record_count, sg_pair_oracle **oracle) { + if (automaton == NULL || records == NULL || oracle == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + *oracle = NULL; + sg_pair_oracle *created = NULL; + sg_status status = sg_oracle_allocate(automaton, &created); + if (status != SG_OK) { + return status; + } + if (record_count != created->pair_count) { + sg_pair_oracle_free(created); + return SG_ERR_INVALID_MODEL; + } + sg_oracle_fill_pairs(created); + bool *seen = calloc(created->pair_count, sizeof(*seen)); + if (seen == NULL) { + sg_pair_oracle_free(created); + return SG_ERR_ALLOC; + } + for (size_t index = 0U; index < record_count; ++index) { + const sg_pair_record record = records[index]; + if (record.pair >= created->pair_count || seen[record.pair]) { + status = SG_ERR_INVALID_MODEL; + break; + } + seen[record.pair] = true; + created->merge_distance[record.pair] = record.mergeable ? record.merge_distance : SG_INDEX_NONE; + created->merge_action[record.pair] = record.mergeable ? record.merge_action : SG_INDEX_NONE; + created->merge_next[record.pair] = record.mergeable ? record.merge_next_pair : SG_INDEX_NONE; + created->resolution_distance[record.pair] = + record.resolvable ? record.resolution_distance : SG_INDEX_NONE; + created->resolution_action[record.pair] = + record.resolvable ? record.resolution_action : SG_INDEX_NONE; + created->resolution_next[record.pair] = + record.resolvable ? record.resolution_next_pair : SG_INDEX_NONE; + } + for (size_t pair = 0U; status == SG_OK && pair < created->pair_count; ++pair) { + if (!seen[pair] || !sg_merge_record_valid(created, pair) || + !sg_resolution_record_valid(created, pair)) { + status = SG_ERR_INVALID_MODEL; + } + } + free(seen); + if (status != SG_OK) { + sg_pair_oracle_free(created); + return status; + } + *oracle = created; + return SG_OK; +} + +static sg_status sg_witness_word(const sg_pair_oracle *oracle, size_t first, size_t second, + bool resolution, sg_word *word) { + if (oracle == NULL || word == NULL || first >= oracle->automaton->state_count || + second >= oracle->automaton->state_count) { + return SG_ERR_INVALID_ARGUMENT; + } + sg_status status = sg_word_init(word); + if (status != SG_OK) { + return status; + } + size_t pair = sg_pair_index(oracle->automaton->state_count, first, second); + const size_t *distances = resolution ? oracle->resolution_distance : oracle->merge_distance; + const size_t *actions = resolution ? oracle->resolution_action : oracle->merge_action; + const size_t *next_pairs = resolution ? oracle->resolution_next : oracle->merge_next; + if (distances[pair] == SG_INDEX_NONE) { + sg_word_free(word); + return SG_ERR_NOT_FOUND; + } + while (distances[pair] != 0U) { + if (actions[pair] == SG_INDEX_NONE) { + sg_word_free(word); + return SG_ERR_INVALID_MODEL; + } + status = sg_word_append(word, actions[pair]); + if (status != SG_OK) { + sg_word_free(word); + return status; + } + if (resolution && next_pairs[pair] == SG_INDEX_NONE) { + break; + } + pair = next_pairs[pair]; + if (pair >= oracle->pair_count) { + sg_word_free(word); + return SG_ERR_INVALID_MODEL; + } + } + return SG_OK; +} + +sg_status sg_pair_oracle_merge_word(const sg_pair_oracle *oracle, size_t first, size_t second, + sg_word *word) { + return sg_witness_word(oracle, first, second, false, word); +} + +sg_status sg_pair_oracle_resolution_word(const sg_pair_oracle *oracle, size_t first, size_t second, + sg_word *word) { + return sg_witness_word(oracle, first, second, true, word); +} diff --git a/src/planner.c b/src/planner.c new file mode 100644 index 0000000..170045e --- /dev/null +++ b/src/planner.c @@ -0,0 +1,1177 @@ +#include "sync_internal.h" + +#include +#include + +#define SG_BITSET_WORD_BITS 64U + +typedef struct { + uint64_t *words; + size_t word_count; + size_t state_count; +} sg_bitset; + +typedef struct { + sg_bitset support; + size_t *trace; + size_t trace_length; +} sg_trace_branch; + +typedef struct { + sg_trace_branch *branches; + size_t count; + size_t capacity; +} sg_trace_partition; + +typedef struct { + sg_bitset *supports; + size_t count; + size_t capacity; +} sg_support_partition; + +typedef struct { + sg_support_partition partition; + size_t parent; + size_t action; +} sg_search_node; + +typedef struct { + sg_search_node *nodes; + size_t count; + size_t capacity; +} sg_search_nodes; + +static sg_status sg_bitset_init(sg_bitset *set, size_t state_count) { + if (set == NULL || state_count == 0U) { + return SG_ERR_INVALID_ARGUMENT; + } + set->state_count = state_count; + set->word_count = (state_count + (SG_BITSET_WORD_BITS - 1U)) / SG_BITSET_WORD_BITS; + set->words = calloc(set->word_count, sizeof(*set->words)); + return set->words == NULL ? SG_ERR_ALLOC : SG_OK; +} + +static void sg_bitset_free(sg_bitset *set) { + if (set == NULL) { + return; + } + free(set->words); + set->words = NULL; + set->word_count = 0U; + set->state_count = 0U; +} + +static void sg_bitset_clear(sg_bitset *set) { + memset(set->words, 0, set->word_count * sizeof(*set->words)); +} + +static void sg_bitset_add(sg_bitset *set, size_t state) { + set->words[state / SG_BITSET_WORD_BITS] |= UINT64_C(1) << (state % SG_BITSET_WORD_BITS); +} + +static bool sg_bitset_has(const sg_bitset *set, size_t state) { + return (set->words[state / SG_BITSET_WORD_BITS] & + (UINT64_C(1) << (state % SG_BITSET_WORD_BITS))) != 0U; +} + +static size_t sg_popcount(uint64_t value) { + size_t count = 0U; + while (value != 0U) { + value &= value - 1U; + ++count; + } + return count; +} + +static size_t sg_bitset_count(const sg_bitset *set) { + size_t count = 0U; + for (size_t index = 0U; index < set->word_count; ++index) { + count += sg_popcount(set->words[index]); + } + return count; +} + +static bool sg_bitset_equal(const sg_bitset *first, const sg_bitset *second) { + return first->state_count == second->state_count && first->word_count == second->word_count && + memcmp(first->words, second->words, first->word_count * sizeof(*first->words)) == 0; +} + +static bool sg_bitset_subset(const sg_bitset *first, const sg_bitset *second) { + if (first->state_count != second->state_count || first->word_count != second->word_count) { + return false; + } + for (size_t index = 0U; index < first->word_count; ++index) { + if ((first->words[index] & ~second->words[index]) != 0U) { + return false; + } + } + return true; +} + +static int sg_bitset_compare(const sg_bitset *first, const sg_bitset *second) { + for (size_t index = first->word_count; index > 0U; --index) { + const uint64_t first_word = first->words[index - 1U]; + const uint64_t second_word = second->words[index - 1U]; + if (first_word < second_word) { + return -1; + } + if (first_word > second_word) { + return 1; + } + } + return 0; +} + +static sg_status sg_bitset_copy(const sg_bitset *source, sg_bitset *destination) { + sg_status status = sg_bitset_init(destination, source->state_count); + if (status == SG_OK) { + memcpy(destination->words, source->words, source->word_count * sizeof(*source->words)); + } + return status; +} + +static sg_status sg_bitset_from_ids(size_t state_count, const size_t *states, size_t count, + sg_bitset *set) { + if (states == NULL || count == 0U || set == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + sg_status status = sg_bitset_init(set, state_count); + if (status != SG_OK) { + return status; + } + for (size_t index = 0U; index < count; ++index) { + if (states[index] >= state_count) { + sg_bitset_free(set); + return SG_ERR_INVALID_ARGUMENT; + } + sg_bitset_add(set, states[index]); + } + return SG_OK; +} + +static void sg_bitset_to_ids(const sg_bitset *set, size_t *states, size_t *count) { + size_t position = 0U; + for (size_t state = 0U; state < set->state_count; ++state) { + if (sg_bitset_has(set, state)) { + states[position] = state; + ++position; + } + } + *count = position; +} + +static void sg_apply_action_set(const sg_automaton *automaton, const sg_bitset *source, + size_t action, sg_bitset *destination) { + sg_bitset_clear(destination); + for (size_t state = 0U; state < automaton->state_count; ++state) { + if (sg_bitset_has(source, state)) { + sg_bitset_add(destination, sg_automaton_transition(automaton, state, action)); + } + } +} + +static sg_status sg_apply_word_set(const sg_automaton *automaton, const sg_bitset *source, + const sg_word *word, sg_bitset *destination) { + sg_bitset current = {0}; + sg_bitset next = {0}; + sg_status status = sg_bitset_copy(source, ¤t); + if (status == SG_OK) { + status = sg_bitset_init(&next, automaton->state_count); + } + if (status != SG_OK) { + sg_bitset_free(¤t); + return status; + } + for (size_t index = 0U; index < word->length; ++index) { + if (word->actions[index] >= automaton->action_count) { + sg_bitset_free(¤t); + sg_bitset_free(&next); + return SG_ERR_INVALID_ARGUMENT; + } + sg_apply_action_set(automaton, ¤t, word->actions[index], &next); + sg_bitset temporary = current; + current = next; + next = temporary; + } + sg_bitset_free(&next); + *destination = current; + return SG_OK; +} + +sg_status sg_apply_word(const sg_automaton *automaton, const size_t *initial_states, + size_t initial_count, const sg_word *word, size_t *output_states, + size_t *output_count) { + if (automaton == NULL || initial_states == NULL || initial_count == 0U || word == NULL || + output_states == NULL || output_count == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + sg_bitset initial = {0}; + sg_bitset final = {0}; + sg_status status = + sg_bitset_from_ids(automaton->state_count, initial_states, initial_count, &initial); + if (status == SG_OK) { + status = sg_apply_word_set(automaton, &initial, word, &final); + } + if (status == SG_OK) { + sg_bitset_to_ids(&final, output_states, output_count); + } + sg_bitset_free(&initial); + sg_bitset_free(&final); + return status; +} + +static void sg_plan_result_reset(sg_plan_result *result) { + result->outcome = SG_OUTCOME_NO_PLAN; + result->method = SG_METHOD_NONE; + result->final_state = SG_INDEX_NONE; + result->final_support_size = 0U; + result->best_support_size = 0U; + result->worst_support_size = 0U; + result->branch_count = 0U; + result->expansions = 0U; + result->homing = false; + result->generation = 0U; + result->planning_time_us = 0U; +} + +static sg_status sg_plan_result_init(const sg_automaton *automaton, sg_plan_result *result) { + if (result == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + sg_plan_result_reset(result); + result->generation = automaton->generation; + return sg_word_init(&result->word); +} + +void sg_plan_result_free(sg_plan_result *result) { + if (result == NULL) { + return; + } + sg_word_free(&result->word); + sg_plan_result_reset(result); +} + +static uint64_t sg_elapsed_us(uint64_t start) { + const uint64_t end = sg_monotonic_time_us(); + return end >= start ? end - start : 0U; +} + +static bool sg_sync_candidate_better(size_t count, size_t length, size_t pair, size_t best_count, + size_t best_length, size_t best_pair) { + return count < best_count || + (count == best_count && + (length < best_length || (length == best_length && pair < best_pair))); +} + +static sg_status sg_best_merge(const sg_automaton *automaton, const sg_pair_oracle *oracle, + const sg_bitset *active, sg_word *best_word, + sg_bitset *best_support) { + size_t best_count = SG_INDEX_NONE; + size_t best_length = SG_INDEX_NONE; + size_t best_pair = SG_INDEX_NONE; + sg_status status = SG_ERR_NOT_FOUND; + for (size_t first = 0U; first < automaton->state_count; ++first) { + if (!sg_bitset_has(active, first)) { + continue; + } + for (size_t second = first + 1U; second < automaton->state_count; ++second) { + if (!sg_bitset_has(active, second)) { + continue; + } + sg_word candidate_word = {0}; + if (sg_pair_oracle_merge_word(oracle, first, second, &candidate_word) != SG_OK) { + continue; + } + sg_bitset candidate_support = {0}; + status = sg_apply_word_set(automaton, active, &candidate_word, &candidate_support); + if (status != SG_OK) { + sg_word_free(&candidate_word); + return status; + } + const size_t count = sg_bitset_count(&candidate_support); + const size_t pair = sg_pair_index(automaton->state_count, first, second); + if (sg_sync_candidate_better(count, candidate_word.length, pair, best_count, best_length, + best_pair)) { + sg_word_free(best_word); + sg_bitset_free(best_support); + *best_word = candidate_word; + *best_support = candidate_support; + best_count = count; + best_length = candidate_word.length; + best_pair = pair; + status = SG_OK; + } else { + sg_word_free(&candidate_word); + sg_bitset_free(&candidate_support); + } + } + } + return best_pair == SG_INDEX_NONE ? SG_ERR_NOT_FOUND : status; +} + +static void sg_trace_partition_free(sg_trace_partition *partition) { + if (partition == NULL) { + return; + } + for (size_t index = 0U; index < partition->count; ++index) { + sg_bitset_free(&partition->branches[index].support); + free(partition->branches[index].trace); + } + free(partition->branches); + partition->branches = NULL; + partition->count = 0U; + partition->capacity = 0U; +} + +static sg_status sg_trace_partition_add(sg_trace_partition *partition, sg_bitset *support, + const size_t *trace, size_t trace_length, + size_t appended_output) { + if (partition->count == partition->capacity) { + const size_t capacity = partition->capacity == 0U ? 8U : partition->capacity * 2U; + size_t bytes = 0U; + if (capacity < partition->capacity || + !sg_size_multiply(capacity, sizeof(*partition->branches), &bytes)) { + return SG_ERR_ALLOC; + } + sg_trace_branch *branches = realloc(partition->branches, bytes); + if (branches == NULL) { + return SG_ERR_ALLOC; + } + partition->branches = branches; + partition->capacity = capacity; + } + size_t *new_trace = NULL; + if (trace_length < SIZE_MAX) { + new_trace = malloc((trace_length + 1U) * sizeof(*new_trace)); + } + if (new_trace == NULL) { + return SG_ERR_ALLOC; + } + if (trace_length != 0U) { + memcpy(new_trace, trace, trace_length * sizeof(*trace)); + } + new_trace[trace_length] = appended_output; + partition->branches[partition->count] = (sg_trace_branch){ + .support = *support, + .trace = new_trace, + .trace_length = trace_length + 1U, + }; + support->words = NULL; + ++partition->count; + return SG_OK; +} + +static sg_status sg_trace_partition_init(const sg_bitset *initial, sg_trace_partition *partition) { + partition->branches = calloc(1U, sizeof(*partition->branches)); + if (partition->branches == NULL) { + return SG_ERR_ALLOC; + } + partition->capacity = 1U; + partition->count = 1U; + const sg_status status = sg_bitset_copy(initial, &partition->branches[0].support); + if (status != SG_OK) { + sg_trace_partition_free(partition); + } + return status; +} + +static sg_status sg_trace_partition_apply_action(const sg_automaton *automaton, + const sg_trace_partition *source, size_t action, + sg_trace_partition *destination) { + for (size_t branch = 0U; branch < source->count; ++branch) { + sg_bitset *groups = calloc(automaton->output_count, sizeof(*groups)); + bool *used = calloc(automaton->output_count, sizeof(*used)); + if (groups == NULL || used == NULL) { + free(groups); + free(used); + sg_trace_partition_free(destination); + return SG_ERR_ALLOC; + } + sg_status status = SG_OK; + for (size_t state = 0U; state < automaton->state_count; ++state) { + if (!sg_bitset_has(&source->branches[branch].support, state)) { + continue; + } + const size_t output = sg_automaton_observation(automaton, state, action); + if (!used[output]) { + status = sg_bitset_init(&groups[output], automaton->state_count); + if (status != SG_OK) { + break; + } + used[output] = true; + } + if (groups[output].words == NULL) { + status = SG_ERR_INVALID_MODEL; + break; + } + sg_bitset_add(&groups[output], sg_automaton_transition(automaton, state, action)); + } + for (size_t output = 0U; status == SG_OK && output < automaton->output_count; ++output) { + if (used[output]) { + status = + sg_trace_partition_add(destination, &groups[output], source->branches[branch].trace, + source->branches[branch].trace_length, output); + } + } + for (size_t output = 0U; output < automaton->output_count; ++output) { + sg_bitset_free(&groups[output]); + } + free(groups); + free(used); + if (status != SG_OK) { + sg_trace_partition_free(destination); + return status; + } + } + return SG_OK; +} + +static sg_status sg_trace_partition_apply_word(const sg_automaton *automaton, + const sg_trace_partition *source, + const sg_word *word, + sg_trace_partition *destination) { + sg_trace_partition current = {0}; + if (source->count != 1U || source->branches[0].trace_length != 0U) { + return SG_ERR_INVALID_ARGUMENT; + } + sg_status status = sg_trace_partition_init(&source->branches[0].support, ¤t); + for (size_t index = 0U; status == SG_OK && index < word->length; ++index) { + sg_trace_partition next = {0}; + status = sg_trace_partition_apply_action(automaton, ¤t, word->actions[index], &next); + sg_trace_partition_free(¤t); + current = next; + } + if (status != SG_OK) { + sg_trace_partition_free(¤t); + return status; + } + *destination = current; + return SG_OK; +} + +static void sg_trace_metrics(const sg_trace_partition *partition, size_t *best, size_t *worst, + size_t *total) { + *best = SG_INDEX_NONE; + *worst = 0U; + *total = 0U; + for (size_t index = 0U; index < partition->count; ++index) { + const size_t count = sg_bitset_count(&partition->branches[index].support); + if (count < *best) { + *best = count; + } + if (count > *worst) { + *worst = count; + } + *total += count; + } + if (partition->count == 0U) { + *best = 0U; + } +} + +static sg_status sg_fill_plan_metrics(const sg_automaton *automaton, const sg_bitset *initial, + sg_plan_result *result) { + sg_trace_partition source = {0}; + sg_trace_partition final = {0}; + sg_status status = sg_trace_partition_init(initial, &source); + if (status == SG_OK) { + status = sg_trace_partition_apply_word(automaton, &source, &result->word, &final); + } + if (status == SG_OK) { + size_t total = 0U; + sg_trace_metrics(&final, &result->best_support_size, &result->worst_support_size, &total); + result->branch_count = final.count; + result->homing = result->worst_support_size <= 1U; + } + sg_trace_partition_free(&source); + sg_trace_partition_free(&final); + return status; +} + +static sg_status sg_sync_finalize(const sg_automaton *automaton, const sg_bitset *initial, + sg_plan_result *result) { + sg_bitset final = {0}; + sg_status status = sg_apply_word_set(automaton, initial, &result->word, &final); + if (status == SG_OK) { + result->final_support_size = sg_bitset_count(&final); + if (result->final_support_size != 1U) { + status = SG_ERR_INVALID_MODEL; + } else { + size_t state = 0U; + size_t count = 0U; + sg_bitset_to_ids(&final, &state, &count); + result->final_state = state; + status = sg_fill_plan_metrics(automaton, initial, result); + } + } + sg_bitset_free(&final); + return status; +} + +sg_status sg_plan_sync(const sg_automaton *automaton, const sg_pair_oracle *oracle, + const size_t *initial_states, size_t initial_count, size_t budget, + sg_plan_result *result) { + if (automaton == NULL || oracle == NULL || oracle->automaton != automaton || + initial_states == NULL || initial_count == 0U || budget == 0U || result == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + const uint64_t start = sg_monotonic_time_us(); + sg_status status = sg_plan_result_init(automaton, result); + if (status != SG_OK) { + return status; + } + sg_bitset initial = {0}; + sg_bitset active = {0}; + status = sg_bitset_from_ids(automaton->state_count, initial_states, initial_count, &initial); + if (status == SG_OK) { + status = sg_bitset_copy(&initial, &active); + } + if (status != SG_OK) { + sg_plan_result_free(result); + sg_bitset_free(&initial); + return status; + } + if (sg_bitset_count(&active) == 1U) { + result->outcome = SG_OUTCOME_ALREADY_SATISFIED; + status = sg_sync_finalize(automaton, &initial, result); + } + while (status == SG_OK && result->outcome != SG_OUTCOME_ALREADY_SATISFIED && + sg_bitset_count(&active) > 1U) { + if (result->expansions >= budget) { + result->outcome = SG_OUTCOME_RESOURCE_BOUND; + break; + } + sg_word witness = {0}; + sg_bitset next = {0}; + status = sg_best_merge(automaton, oracle, &active, &witness, &next); + if (status == SG_ERR_NOT_FOUND) { + result->outcome = SG_OUTCOME_NO_PLAN; + status = SG_OK; + break; + } + if (status == SG_OK) { + status = sg_word_extend(&result->word, &witness); + } + sg_word_free(&witness); + if (status == SG_OK) { + sg_bitset_free(&active); + active = next; + ++result->expansions; + } else { + sg_bitset_free(&next); + } + } + if (status == SG_OK && result->outcome != SG_OUTCOME_ALREADY_SATISFIED && + sg_bitset_count(&active) == 1U) { + result->outcome = SG_OUTCOME_PLAN; + result->method = SG_METHOD_PAIR_MERGE; + status = sg_sync_finalize(automaton, &initial, result); + } + result->planning_time_us = sg_elapsed_us(start); + sg_bitset_free(&initial); + sg_bitset_free(&active); + if (status != SG_OK) { + sg_plan_result_free(result); + } + return status; +} + +static bool sg_resolution_candidate_better(size_t worst, size_t length, size_t branches, + size_t pair, size_t best_worst, size_t best_length, + size_t best_branches, size_t best_pair) { + return worst < best_worst || + (worst == best_worst && + (length < best_length || + (length == best_length && + (branches > best_branches || (branches == best_branches && pair < best_pair))))); +} + +static sg_status sg_best_resolution(const sg_automaton *automaton, const sg_pair_oracle *oracle, + const sg_bitset *initial, size_t current_worst, + sg_word *best_word, sg_trace_partition *best_partition) { + sg_trace_partition source = {0}; + sg_status status = sg_trace_partition_init(initial, &source); + if (status != SG_OK) { + return status; + } + size_t best_worst = SG_INDEX_NONE; + size_t best_length = SG_INDEX_NONE; + size_t best_branches = 0U; + size_t best_pair = SG_INDEX_NONE; + for (size_t first = 0U; first < automaton->state_count; ++first) { + if (!sg_bitset_has(initial, first)) { + continue; + } + for (size_t second = first + 1U; second < automaton->state_count; ++second) { + if (!sg_bitset_has(initial, second)) { + continue; + } + sg_word candidate_word = {0}; + if (sg_pair_oracle_resolution_word(oracle, first, second, &candidate_word) != SG_OK) { + continue; + } + sg_trace_partition candidate_partition = {0}; + status = + sg_trace_partition_apply_word(automaton, &source, &candidate_word, &candidate_partition); + if (status != SG_OK) { + sg_word_free(&candidate_word); + break; + } + size_t best = 0U; + size_t worst = 0U; + size_t total = 0U; + sg_trace_metrics(&candidate_partition, &best, &worst, &total); + const size_t pair = sg_pair_index(automaton->state_count, first, second); + if (sg_resolution_candidate_better(worst, candidate_word.length, candidate_partition.count, + pair, best_worst, best_length, best_branches, best_pair)) { + sg_word_free(best_word); + sg_trace_partition_free(best_partition); + *best_word = candidate_word; + *best_partition = candidate_partition; + best_worst = worst; + best_length = candidate_word.length; + best_branches = candidate_partition.count; + best_pair = pair; + } else { + sg_word_free(&candidate_word); + sg_trace_partition_free(&candidate_partition); + } + } + if (status != SG_OK) { + break; + } + } + sg_trace_partition_free(&source); + if (status != SG_OK) { + sg_word_free(best_word); + sg_trace_partition_free(best_partition); + return status; + } + return best_pair != SG_INDEX_NONE && best_worst < current_worst ? SG_OK : SG_ERR_NOT_FOUND; +} + +static void sg_support_partition_free(sg_support_partition *partition) { + if (partition == NULL) { + return; + } + for (size_t index = 0U; index < partition->count; ++index) { + sg_bitset_free(&partition->supports[index]); + } + free(partition->supports); + partition->supports = NULL; + partition->count = 0U; + partition->capacity = 0U; +} + +static sg_status sg_support_partition_add(sg_support_partition *partition, sg_bitset *support) { + for (size_t index = 0U; index < partition->count; ++index) { + if (sg_bitset_equal(&partition->supports[index], support)) { + sg_bitset_free(support); + return SG_OK; + } + } + if (partition->count == partition->capacity) { + const size_t capacity = partition->capacity == 0U ? 8U : partition->capacity * 2U; + size_t bytes = 0U; + if (capacity < partition->capacity || + !sg_size_multiply(capacity, sizeof(*partition->supports), &bytes)) { + return SG_ERR_ALLOC; + } + sg_bitset *supports = realloc(partition->supports, bytes); + if (supports == NULL) { + return SG_ERR_ALLOC; + } + partition->supports = supports; + partition->capacity = capacity; + } + size_t position = partition->count; + while (position > 0U && sg_bitset_compare(support, &partition->supports[position - 1U]) < 0) { + partition->supports[position] = partition->supports[position - 1U]; + --position; + } + partition->supports[position] = *support; + support->words = NULL; + ++partition->count; + return SG_OK; +} + +static sg_status sg_support_partition_init(const sg_bitset *initial, + sg_support_partition *partition) { + sg_bitset copy = {0}; + sg_status status = sg_bitset_copy(initial, ©); + if (status == SG_OK) { + status = sg_support_partition_add(partition, ©); + } + sg_bitset_free(©); + return status; +} + +static size_t sg_support_partition_worst(const sg_support_partition *partition) { + size_t worst = 0U; + for (size_t index = 0U; index < partition->count; ++index) { + const size_t count = sg_bitset_count(&partition->supports[index]); + if (count > worst) { + worst = count; + } + } + return worst; +} + +static bool sg_support_partition_equal(const sg_support_partition *first, + const sg_support_partition *second) { + if (first->count != second->count) { + return false; + } + for (size_t index = 0U; index < first->count; ++index) { + if (!sg_bitset_equal(&first->supports[index], &second->supports[index])) { + return false; + } + } + return true; +} + +static sg_status sg_support_partition_apply_action(const sg_automaton *automaton, + const sg_support_partition *source, + size_t action, + sg_support_partition *destination) { + for (size_t branch = 0U; branch < source->count; ++branch) { + sg_bitset *groups = calloc(automaton->output_count, sizeof(*groups)); + bool *used = calloc(automaton->output_count, sizeof(*used)); + if (groups == NULL || used == NULL) { + free(groups); + free(used); + sg_support_partition_free(destination); + return SG_ERR_ALLOC; + } + sg_status status = SG_OK; + for (size_t state = 0U; state < automaton->state_count; ++state) { + if (!sg_bitset_has(&source->supports[branch], state)) { + continue; + } + const size_t output = sg_automaton_observation(automaton, state, action); + if (!used[output]) { + status = sg_bitset_init(&groups[output], automaton->state_count); + if (status != SG_OK) { + break; + } + used[output] = true; + } + if (groups[output].words == NULL) { + status = SG_ERR_INVALID_MODEL; + break; + } + sg_bitset_add(&groups[output], sg_automaton_transition(automaton, state, action)); + } + for (size_t output = 0U; status == SG_OK && output < automaton->output_count; ++output) { + if (used[output]) { + status = sg_support_partition_add(destination, &groups[output]); + } + } + for (size_t output = 0U; output < automaton->output_count; ++output) { + sg_bitset_free(&groups[output]); + } + free(groups); + free(used); + if (status != SG_OK) { + sg_support_partition_free(destination); + return status; + } + } + return SG_OK; +} + +static void sg_search_nodes_free(sg_search_nodes *search) { + if (search == NULL) { + return; + } + for (size_t index = 0U; index < search->count; ++index) { + sg_support_partition_free(&search->nodes[index].partition); + } + free(search->nodes); + search->nodes = NULL; + search->count = 0U; + search->capacity = 0U; +} + +static sg_status sg_search_nodes_add(sg_search_nodes *search, sg_support_partition *partition, + size_t parent, size_t action, size_t *index) { + if (search->count == search->capacity) { + const size_t capacity = search->capacity == 0U ? 16U : search->capacity * 2U; + size_t bytes = 0U; + if (capacity < search->capacity || + !sg_size_multiply(capacity, sizeof(*search->nodes), &bytes)) { + return SG_ERR_ALLOC; + } + sg_search_node *nodes = realloc(search->nodes, bytes); + if (nodes == NULL) { + return SG_ERR_ALLOC; + } + search->nodes = nodes; + search->capacity = capacity; + } + *index = search->count; + search->nodes[search->count] = (sg_search_node){ + .partition = *partition, + .parent = parent, + .action = action, + }; + partition->supports = NULL; + partition->count = 0U; + partition->capacity = 0U; + ++search->count; + return SG_OK; +} + +static size_t sg_search_find(const sg_search_nodes *search, const sg_support_partition *partition) { + for (size_t index = 0U; index < search->count; ++index) { + if (sg_support_partition_equal(&search->nodes[index].partition, partition)) { + return index; + } + } + return SG_INDEX_NONE; +} + +static sg_status sg_search_reconstruct(const sg_search_nodes *search, size_t goal, sg_word *word) { + sg_status status = sg_word_init(word); + size_t cursor = goal; + while (status == SG_OK && search->nodes[cursor].parent != SG_INDEX_NONE) { + status = sg_word_append(word, search->nodes[cursor].action); + cursor = search->nodes[cursor].parent; + } + for (size_t first = 0U, second = word->length; first < second && second != 0U; ++first) { + --second; + const size_t temporary = word->actions[first]; + word->actions[first] = word->actions[second]; + word->actions[second] = temporary; + } + if (status != SG_OK) { + sg_word_free(word); + } + return status; +} + +static sg_status sg_partition_bfs(const sg_automaton *automaton, const sg_bitset *initial, + size_t bound, size_t budget, size_t *expansions, sg_word *word, + sg_plan_outcome *outcome) { + sg_search_nodes search = {0}; + sg_support_partition root = {0}; + sg_status status = sg_support_partition_init(initial, &root); + size_t root_index = 0U; + if (status == SG_OK) { + status = sg_search_nodes_add(&search, &root, SG_INDEX_NONE, SG_INDEX_NONE, &root_index); + } + size_t head = 0U; + size_t goal = SG_INDEX_NONE; + while (status == SG_OK && head < search.count && goal == SG_INDEX_NONE) { + if (*expansions >= budget) { + *outcome = SG_OUTCOME_RESOURCE_BOUND; + break; + } + const size_t parent = head; + ++head; + ++*expansions; + for (size_t action = 0U; action < automaton->action_count; ++action) { + sg_support_partition next = {0}; + status = sg_support_partition_apply_action(automaton, &search.nodes[parent].partition, action, + &next); + if (status != SG_OK) { + break; + } + if (sg_search_find(&search, &next) != SG_INDEX_NONE) { + sg_support_partition_free(&next); + continue; + } + size_t next_index = 0U; + status = sg_search_nodes_add(&search, &next, parent, action, &next_index); + if (status != SG_OK) { + sg_support_partition_free(&next); + break; + } + if (sg_support_partition_worst(&search.nodes[next_index].partition) <= bound) { + goal = next_index; + break; + } + } + } + if (status == SG_OK && goal != SG_INDEX_NONE) { + status = sg_search_reconstruct(&search, goal, word); + *outcome = SG_OUTCOME_PLAN; + } else if (status == SG_OK && *outcome != SG_OUTCOME_RESOURCE_BOUND) { + *outcome = SG_OUTCOME_NO_PLAN; + } + sg_support_partition_free(&root); + sg_search_nodes_free(&search); + return status; +} + +static sg_status sg_disambiguation_finalize(const sg_automaton *automaton, const sg_bitset *initial, + size_t bound, sg_plan_result *result) { + sg_status status = sg_fill_plan_metrics(automaton, initial, result); + if (status == SG_OK && result->worst_support_size > bound) { + return SG_ERR_INVALID_MODEL; + } + result->final_support_size = result->worst_support_size; + return status; +} + +sg_status sg_plan_disambiguate(const sg_automaton *automaton, const sg_pair_oracle *oracle, + const size_t *initial_states, size_t initial_count, size_t bound, + size_t budget, sg_plan_result *result) { + if (automaton == NULL || oracle == NULL || oracle->automaton != automaton || + initial_states == NULL || initial_count == 0U || bound == 0U || budget == 0U || + result == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + const uint64_t start = sg_monotonic_time_us(); + sg_status status = sg_plan_result_init(automaton, result); + if (status != SG_OK) { + return status; + } + sg_bitset initial = {0}; + status = sg_bitset_from_ids(automaton->state_count, initial_states, initial_count, &initial); + const size_t unique_count = status == SG_OK ? sg_bitset_count(&initial) : 0U; + if (status != SG_OK || bound > unique_count) { + sg_plan_result_free(result); + sg_bitset_free(&initial); + return status == SG_OK ? SG_ERR_INVALID_ARGUMENT : status; + } + if (unique_count <= bound) { + result->outcome = SG_OUTCOME_ALREADY_SATISFIED; + status = sg_disambiguation_finalize(automaton, &initial, bound, result); + } else { + sg_word heuristic = {0}; + sg_trace_partition heuristic_partition = {0}; + status = sg_best_resolution(automaton, oracle, &initial, unique_count, &heuristic, + &heuristic_partition); + if (status == SG_OK) { + size_t best = 0U; + size_t worst = 0U; + size_t total = 0U; + sg_trace_metrics(&heuristic_partition, &best, &worst, &total); + ++result->expansions; + if (worst <= bound) { + result->word = heuristic; + result->outcome = SG_OUTCOME_PLAN; + result->method = SG_METHOD_PAIR_RESOLUTION; + status = sg_disambiguation_finalize(automaton, &initial, bound, result); + } else { + sg_word_free(&heuristic); + status = SG_ERR_NOT_FOUND; + } + } + sg_trace_partition_free(&heuristic_partition); + if (status == SG_ERR_NOT_FOUND) { + status = sg_partition_bfs(automaton, &initial, bound, budget, &result->expansions, + &result->word, &result->outcome); + if (status == SG_OK && result->outcome == SG_OUTCOME_PLAN) { + result->method = SG_METHOD_PARTITION_BFS; + status = sg_disambiguation_finalize(automaton, &initial, bound, result); + } + } + } + result->planning_time_us = sg_elapsed_us(start); + sg_bitset_free(&initial); + if (status != SG_OK) { + sg_plan_result_free(result); + } + return status; +} + +static sg_status sg_explain_partition(const sg_trace_partition *partition, + const sg_bitset *predicted, size_t step, size_t action, + sg_explain_visitor visitor, void *context) { + const size_t predicted_count = sg_bitset_count(predicted); + size_t *predicted_states = malloc(predicted_count * sizeof(*predicted_states)); + if (predicted_states == NULL) { + return SG_ERR_ALLOC; + } + size_t converted = 0U; + sg_bitset_to_ids(predicted, predicted_states, &converted); + sg_status status = SG_OK; + for (size_t branch = 0U; status == SG_OK && branch < partition->count; ++branch) { + const size_t branch_count = sg_bitset_count(&partition->branches[branch].support); + size_t *branch_states = malloc(branch_count * sizeof(*branch_states)); + if (branch_states == NULL) { + status = SG_ERR_ALLOC; + break; + } + sg_bitset_to_ids(&partition->branches[branch].support, branch_states, &converted); + status = visitor(context, step, action, predicted_states, predicted_count, + partition->branches[branch].trace, partition->branches[branch].trace_length, + branch_states, branch_count); + free(branch_states); + } + free(predicted_states); + return status; +} + +sg_status sg_explain_plan(const sg_automaton *automaton, uint64_t plan_generation, + const size_t *initial_states, size_t initial_count, const sg_word *word, + sg_explain_visitor visitor, void *context) { + if (automaton == NULL || initial_states == NULL || initial_count == 0U || word == NULL || + visitor == NULL) { + return SG_ERR_INVALID_ARGUMENT; + } + if (plan_generation != automaton->generation) { + return SG_ERR_STALE_GENERATION; + } + sg_bitset predicted = {0}; + sg_bitset next_predicted = {0}; + sg_trace_partition partition = {0}; + sg_status status = + sg_bitset_from_ids(automaton->state_count, initial_states, initial_count, &predicted); + if (status == SG_OK) { + status = sg_bitset_init(&next_predicted, automaton->state_count); + } + if (status == SG_OK) { + status = sg_trace_partition_init(&predicted, &partition); + } + if (status == SG_OK) { + status = sg_explain_partition(&partition, &predicted, 0U, SG_INDEX_NONE, visitor, context); + } + for (size_t index = 0U; status == SG_OK && index < word->length; ++index) { + if (word->actions[index] >= automaton->action_count) { + status = SG_ERR_INVALID_ARGUMENT; + break; + } + sg_apply_action_set(automaton, &predicted, word->actions[index], &next_predicted); + sg_bitset temporary = predicted; + predicted = next_predicted; + next_predicted = temporary; + sg_trace_partition next_partition = {0}; + status = sg_trace_partition_apply_action(automaton, &partition, word->actions[index], + &next_partition); + sg_trace_partition_free(&partition); + partition = next_partition; + if (status == SG_OK) { + status = sg_explain_partition(&partition, &predicted, index + 1U, word->actions[index], + visitor, context); + } + } + sg_bitset_free(&predicted); + sg_bitset_free(&next_predicted); + sg_trace_partition_free(&partition); + return status; +} + +void sg_monitor_result_free(sg_monitor_result *result) { + if (result == NULL) { + return; + } + free(result->expected_states); + free(result->unexpected_states); + result->expected_states = NULL; + result->unexpected_states = NULL; + result->expected_count = 0U; + result->unexpected_count = 0U; + result->generation = 0U; + result->decision = SG_MONITOR_WAIT; +} + +static sg_status sg_monitor_fill_arrays(const sg_bitset *expected, const sg_bitset *reported, + sg_monitor_result *result) { + result->expected_count = sg_bitset_count(expected); + result->expected_states = malloc(result->expected_count * sizeof(*result->expected_states)); + if (result->expected_states == NULL) { + return SG_ERR_ALLOC; + } + size_t converted = 0U; + sg_bitset_to_ids(expected, result->expected_states, &converted); + sg_bitset unexpected = {0}; + sg_status status = sg_bitset_init(&unexpected, expected->state_count); + if (status != SG_OK) { + return status; + } + for (size_t state = 0U; state < expected->state_count; ++state) { + if (sg_bitset_has(reported, state) && !sg_bitset_has(expected, state)) { + sg_bitset_add(&unexpected, state); + } + } + result->unexpected_count = sg_bitset_count(&unexpected); + if (result->unexpected_count != 0U) { + result->unexpected_states = + malloc(result->unexpected_count * sizeof(*result->unexpected_states)); + if (result->unexpected_states == NULL) { + sg_bitset_free(&unexpected); + return SG_ERR_ALLOC; + } + sg_bitset_to_ids(&unexpected, result->unexpected_states, &converted); + } + sg_bitset_free(&unexpected); + return SG_OK; +} + +sg_status sg_validate_update(const sg_automaton *automaton, uint64_t plan_generation, + const size_t *initial_states, size_t initial_count, + const sg_word *word, size_t completed_steps, + const size_t *reported_states, size_t reported_count, + bool localizer_available, sg_monitor_result *result) { + if (automaton == NULL || initial_states == NULL || initial_count == 0U || word == NULL || + completed_steps > word->length || result == NULL || + (localizer_available && (reported_states == NULL || reported_count == 0U))) { + return SG_ERR_INVALID_ARGUMENT; + } + *result = (sg_monitor_result){.decision = SG_MONITOR_WAIT, .generation = automaton->generation}; + if (plan_generation != automaton->generation) { + result->decision = SG_MONITOR_STALE_GENERATION; + return SG_OK; + } + sg_bitset expected = {0}; + sg_bitset next = {0}; + sg_status status = + sg_bitset_from_ids(automaton->state_count, initial_states, initial_count, &expected); + if (status == SG_OK) { + status = sg_bitset_init(&next, automaton->state_count); + } + for (size_t index = 0U; status == SG_OK && index < completed_steps; ++index) { + if (word->actions[index] >= automaton->action_count) { + status = SG_ERR_INVALID_ARGUMENT; + break; + } + sg_apply_action_set(automaton, &expected, word->actions[index], &next); + sg_bitset temporary = expected; + expected = next; + next = temporary; + } + if (status != SG_OK) { + sg_bitset_free(&expected); + sg_bitset_free(&next); + return status; + } + if (!localizer_available) { + result->expected_count = sg_bitset_count(&expected); + if (result->expected_count == 0U) { + status = SG_ERR_INVALID_MODEL; + } else { + result->expected_states = calloc(result->expected_count, sizeof(*result->expected_states)); + if (result->expected_states == NULL) { + status = SG_ERR_ALLOC; + } else { + size_t converted = 0U; + sg_bitset_to_ids(&expected, result->expected_states, &converted); + } + } + } else { + sg_bitset reported = {0}; + status = sg_bitset_from_ids(automaton->state_count, reported_states, reported_count, &reported); + if (status == SG_OK) { + status = sg_monitor_fill_arrays(&expected, &reported, result); + } + if (status == SG_OK) { + if (sg_bitset_equal(&reported, &expected)) { + result->decision = SG_MONITOR_CONTINUE; + } else if (sg_bitset_subset(&reported, &expected)) { + result->decision = SG_MONITOR_REPLAN; + } else { + result->decision = SG_MONITOR_MODEL_VIOLATION; + } + } + sg_bitset_free(&reported); + } + sg_bitset_free(&expected); + sg_bitset_free(&next); + if (status != SG_OK) { + sg_monitor_result_free(result); + } + return status; +} diff --git a/src/sync.c b/src/sync.c index 7360548..447e16d 100644 --- a/src/sync.c +++ b/src/sync.c @@ -1,76 +1,69 @@ -#include "sync_kgraph/sync.h" +#include "sync_internal.h" -#include -#include +#include #include #include - -enum { SG_BITS_PER_WORD = 64 }; - -typedef struct { - char **items; - size_t length; - size_t capacity; -} sg_string_vec; +#include typedef struct { char *source; - char *letter; - char *target; -} sg_transition_record; - -typedef struct { - sg_transition_record *items; - size_t length; - size_t capacity; -} sg_transition_vec; - -struct sg_dfa_builder { - sg_string_vec states; - sg_string_vec letters; - sg_transition_vec transitions; -}; + char *action; + char *value; +} sg_builder_entry; -struct sg_dfa { +struct sg_automaton_builder { char **states; - char **letters; size_t state_count; - size_t letter_count; - size_t *transitions; -}; - -struct sg_pair_oracle { - const sg_dfa *dfa; - size_t pair_count; - size_t edge_count; - size_t mergeable_pairs; - size_t *forward; - size_t *reverse_head; - size_t *reverse_next; - size_t *reverse_from; - size_t *reverse_letter; - size_t *dist; - size_t *next_pair; - size_t *witness; + size_t state_capacity; + char **actions; + size_t action_count; + size_t action_capacity; + char **outputs; + size_t output_count; + size_t output_capacity; + sg_builder_entry *transitions; + size_t transition_count; + size_t transition_capacity; + sg_builder_entry *observations; + size_t observation_count; + size_t observation_capacity; }; -typedef struct { - uint64_t *words; - size_t word_count; -} sg_bitset; - -typedef struct { - sg_bitset set; - sg_word word; -} sg_subset_node; +bool sg_size_multiply(size_t first, size_t second, size_t *product) { + if (product == NULL || (first != 0U && second > (SIZE_MAX / first))) { + return false; + } + *product = first * second; + return true; +} -typedef struct { - sg_subset_node *items; - size_t length; - size_t capacity; -} sg_subset_vec; +char *sg_string_duplicate(const char *value) { + if (value == NULL) { + return NULL; + } + const size_t length = strlen(value); + if (length == SIZE_MAX) { + return NULL; + } + char *copy = malloc(length + 1U); + if (copy != NULL) { + memcpy(copy, value, length + 1U); + } + return copy; +} -static const size_t SG_INVALID = (size_t)-1; +uint64_t sg_monotonic_time_us(void) { + struct timespec value = {0}; + if (clock_gettime(CLOCK_MONOTONIC, &value) != 0) { + return 0U; + } + const uint64_t seconds = (uint64_t)value.tv_sec; + const uint64_t nanoseconds = (uint64_t)value.tv_nsec; + if (seconds > (UINT64_MAX / UINT64_C(1000000))) { + return UINT64_MAX; + } + return (seconds * UINT64_C(1000000)) + (nanoseconds / UINT64_C(1000)); +} const char *sg_status_name(sg_status status) { switch (status) { @@ -88,1671 +81,504 @@ const char *sg_status_name(sg_status status) { return "INCOMPLETE"; case SG_ERR_NONDETERMINISTIC: return "NONDETERMINISTIC"; - case SG_ERR_UNSYNCHRONIZABLE: - return "UNSYNCHRONIZABLE"; + case SG_ERR_INVALID_MODEL: + return "INVALID_MODEL"; case SG_ERR_RESOURCE_BOUND: return "RESOURCE_BOUND"; + case SG_ERR_STALE_GENERATION: + return "STALE_GENERATION"; } return "UNKNOWN"; } -const char *sg_result_kind_name(sg_result_kind kind) { - switch (kind) { - case SG_RESULT_TRIVIAL: - return "TRIVIAL"; - case SG_RESULT_PAIR_GREEDY: - return "PAIR_GREEDY"; - case SG_RESULT_PAIR_GREEDY_TARGETED: - return "PAIR_GREEDY_TARGETED"; - case SG_RESULT_EXACT_EXPANDED: - return "EXACT_EXPANDED"; - case SG_RESULT_RESOURCE_BOUND: +const char *sg_plan_outcome_name(sg_plan_outcome outcome) { + switch (outcome) { + case SG_OUTCOME_PLAN: + return "PLAN"; + case SG_OUTCOME_ALREADY_SATISFIED: + return "ALREADY_SATISFIED"; + case SG_OUTCOME_NO_PLAN: + return "NO_PLAN"; + case SG_OUTCOME_RESOURCE_BOUND: return "RESOURCE_BOUND"; - case SG_RESULT_FAILURE: - return "FAILURE"; } return "UNKNOWN"; } -const char *sg_mode_name(sg_mode mode) { - switch (mode) { - case SG_MODE_SYNC: - return "SYNC"; - case SG_MODE_REACH: - return "REACH"; - case SG_MODE_REACH_AND_SYNC: - return "REACH_AND_SYNC"; +const char *sg_plan_method_name(sg_plan_method method) { + switch (method) { + case SG_METHOD_NONE: + return "NONE"; + case SG_METHOD_PAIR_MERGE: + return "PAIR_MERGE"; + case SG_METHOD_PAIR_RESOLUTION: + return "PAIR_RESOLUTION"; + case SG_METHOD_PARTITION_BFS: + return "PARTITION_BFS"; } return "UNKNOWN"; } -sg_status sg_mode_parse(const char *name, sg_mode *mode) { - if (name == NULL || mode == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - if (strcmp(name, "SYNC") == 0) { - *mode = SG_MODE_SYNC; - return SG_OK; - } - if (strcmp(name, "REACH") == 0) { - *mode = SG_MODE_REACH; - return SG_OK; - } - if (strcmp(name, "REACH_AND_SYNC") == 0 || strcmp(name, "REACHSYNC") == 0 || - strcmp(name, "REACH_SYNC") == 0) { - *mode = SG_MODE_REACH_AND_SYNC; - return SG_OK; - } - return SG_ERR_INVALID_ARGUMENT; -} - -static char *sg_strdup_owned(const char *value) { - if (value == NULL) { - return NULL; - } - const size_t length = strlen(value); - char *copy = malloc(length + 1U); - if (copy == NULL) { - return NULL; +const char *sg_monitor_decision_name(sg_monitor_decision decision) { + switch (decision) { + case SG_MONITOR_CONTINUE: + return "CONTINUE"; + case SG_MONITOR_REPLAN: + return "REPLAN"; + case SG_MONITOR_MODEL_VIOLATION: + return "MODEL_VIOLATION"; + case SG_MONITOR_STALE_GENERATION: + return "STALE_GENERATION"; + case SG_MONITOR_WAIT: + return "WAIT"; } - memcpy(copy, value, length + 1U); - return copy; + return "UNKNOWN"; } -static sg_status sg_checked_mul(size_t lhs, size_t rhs, size_t *result) { - if (result == NULL) { +sg_status sg_word_init(sg_word *word) { + if (word == NULL) { return SG_ERR_INVALID_ARGUMENT; } - if (lhs != 0U && rhs > SIZE_MAX / lhs) { - return SG_ERR_ALLOC; - } - *result = lhs * rhs; + word->actions = NULL; + word->length = 0U; + word->capacity = 0U; return SG_OK; } -static sg_status sg_grow(void **items, size_t item_size, size_t *capacity, size_t needed) { - if (items == NULL || capacity == NULL || item_size == 0U) { - return SG_ERR_INVALID_ARGUMENT; - } - if (*capacity >= needed) { - return SG_OK; +void sg_word_free(sg_word *word) { + if (word == NULL) { + return; } + free(word->actions); + word->actions = NULL; + word->length = 0U; + word->capacity = 0U; +} - size_t next = (*capacity == 0U) ? 8U : *capacity; - while (next < needed) { - if (next > SIZE_MAX / 2U) { - return SG_ERR_ALLOC; - } - next *= 2U; +static sg_status sg_word_reserve(sg_word *word, size_t capacity) { + if (capacity <= word->capacity) { + return SG_OK; } - size_t bytes = 0U; - sg_status status = sg_checked_mul(next, item_size, &bytes); - if (status != SG_OK) { - return status; + if (!sg_size_multiply(capacity, sizeof(*word->actions), &bytes)) { + return SG_ERR_ALLOC; } - - void *grown = realloc(*items, bytes); - if (grown == NULL) { + size_t *actions = realloc(word->actions, bytes); + if (actions == NULL) { return SG_ERR_ALLOC; } - *items = grown; - *capacity = next; + word->actions = actions; + word->capacity = capacity; return SG_OK; } -static void sg_string_vec_free(sg_string_vec *vec) { - if (vec == NULL) { - return; - } - for (size_t i = 0U; i < vec->length; ++i) { - free(vec->items[i]); - } - free((void *)vec->items); - vec->items = NULL; - vec->length = 0U; - vec->capacity = 0U; -} - -static sg_status sg_string_vec_find(const sg_string_vec *vec, const char *value, size_t *index) { - if (vec == NULL || value == NULL) { +sg_status sg_word_append(sg_word *word, size_t action) { + if (word == NULL) { return SG_ERR_INVALID_ARGUMENT; } - for (size_t i = 0U; i < vec->length; ++i) { - if (strcmp(vec->items[i], value) == 0) { - if (index != NULL) { - *index = i; - } - return SG_OK; + if (word->length == word->capacity) { + const size_t capacity = word->capacity == 0U ? 8U : word->capacity * 2U; + if (capacity < word->capacity) { + return SG_ERR_ALLOC; + } + const sg_status status = sg_word_reserve(word, capacity); + if (status != SG_OK) { + return status; } } - return SG_ERR_NOT_FOUND; + word->actions[word->length] = action; + ++word->length; + return SG_OK; } -static sg_status sg_string_vec_add_unique(sg_string_vec *vec, const char *value, size_t *index) { - if (vec == NULL || value == NULL || value[0] == '\0') { +sg_status sg_word_extend(sg_word *destination, const sg_word *source) { + if (destination == NULL || source == NULL) { return SG_ERR_INVALID_ARGUMENT; } - size_t existing = 0U; - if (sg_string_vec_find(vec, value, &existing) == SG_OK) { - if (index != NULL) { - *index = existing; - } - return SG_OK; + if (source->length > (SIZE_MAX - destination->length)) { + return SG_ERR_ALLOC; } - - sg_status status = - sg_grow((void **)&vec->items, sizeof(vec->items[0]), &vec->capacity, vec->length + 1U); + const size_t required = destination->length + source->length; + sg_status status = sg_word_reserve(destination, required); if (status != SG_OK) { return status; } - - char *copy = sg_strdup_owned(value); - if (copy == NULL) { - return SG_ERR_ALLOC; - } - vec->items[vec->length] = copy; - if (index != NULL) { - *index = vec->length; + if (source->length != 0U) { + memcpy(&destination->actions[destination->length], source->actions, + source->length * sizeof(*source->actions)); } - ++vec->length; + destination->length = required; return SG_OK; } -static void sg_transition_record_free(sg_transition_record *record) { - if (record == NULL) { +static void sg_string_array_free(char **values, size_t count) { + if (values == NULL) { return; } - free(record->source); - free(record->letter); - free(record->target); - record->source = NULL; - record->letter = NULL; - record->target = NULL; + for (size_t index = 0U; index < count; ++index) { + free(values[index]); + } + free(values); } -static void sg_transition_vec_free(sg_transition_vec *vec) { - if (vec == NULL) { +static void sg_builder_entries_free(sg_builder_entry *entries, size_t count) { + if (entries == NULL) { return; } - for (size_t i = 0U; i < vec->length; ++i) { - sg_transition_record_free(&vec->items[i]); + for (size_t index = 0U; index < count; ++index) { + free(entries[index].source); + free(entries[index].action); + free(entries[index].value); } - free(vec->items); - vec->items = NULL; - vec->length = 0U; - vec->capacity = 0U; + free(entries); } -sg_status sg_word_init(sg_word *word) { - if (word == NULL) { +sg_status sg_automaton_builder_init(sg_automaton_builder **builder) { + if (builder == NULL) { return SG_ERR_INVALID_ARGUMENT; } - word->letters = NULL; - word->length = 0U; - word->capacity = 0U; - return SG_OK; + *builder = calloc(1U, sizeof(**builder)); + return *builder == NULL ? SG_ERR_ALLOC : SG_OK; } -void sg_word_free(sg_word *word) { - if (word == NULL) { +void sg_automaton_builder_free(sg_automaton_builder *builder) { + if (builder == NULL) { return; } - free(word->letters); - word->letters = NULL; - word->length = 0U; - word->capacity = 0U; -} - -sg_status sg_word_append(sg_word *word, size_t letter) { - if (word == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - sg_status status = sg_grow((void **)&word->letters, sizeof(word->letters[0]), &word->capacity, - word->length + 1U); - if (status != SG_OK) { - return status; - } - word->letters[word->length] = letter; - ++word->length; - return SG_OK; -} - -sg_status sg_word_prepend(sg_word *word, size_t letter) { - if (word == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - sg_status status = sg_grow((void **)&word->letters, sizeof(word->letters[0]), &word->capacity, - word->length + 1U); - if (status != SG_OK) { - return status; - } - memmove(&word->letters[1], &word->letters[0], word->length * sizeof(word->letters[0])); - word->letters[0] = letter; - ++word->length; - return SG_OK; + sg_string_array_free(builder->states, builder->state_count); + sg_string_array_free(builder->actions, builder->action_count); + sg_string_array_free(builder->outputs, builder->output_count); + sg_builder_entries_free(builder->transitions, builder->transition_count); + sg_builder_entries_free(builder->observations, builder->observation_count); + free(builder); } -static sg_status sg_word_extend(sg_word *dst, const sg_word *src) { - if (dst == NULL || src == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - for (size_t i = 0U; i < src->length; ++i) { - sg_status status = sg_word_append(dst, src->letters[i]); - if (status != SG_OK) { - return status; +static size_t sg_string_array_find(char *const *values, size_t count, const char *value) { + for (size_t index = 0U; index < count; ++index) { + if (strcmp(values[index], value) == 0) { + return index; } } - return SG_OK; + return SG_INDEX_NONE; } -static sg_status sg_word_copy(const sg_word *src, sg_word *dst) { - if (src == NULL || dst == NULL) { +static sg_status sg_string_array_add(char ***values, size_t *count, size_t *capacity, + const char *value) { + if (values == NULL || count == NULL || capacity == NULL || value == NULL || value[0] == '\0') { return SG_ERR_INVALID_ARGUMENT; } - sg_status status = sg_word_init(dst); - if (status != SG_OK) { - return status; - } - status = sg_word_extend(dst, src); - if (status != SG_OK) { - sg_word_free(dst); + if (sg_string_array_find(*values, *count, value) != SG_INDEX_NONE) { + return SG_ERR_DUPLICATE; } - return status; -} - -sg_status sg_dfa_builder_init(sg_dfa_builder **builder) { - if (builder == NULL) { - return SG_ERR_INVALID_ARGUMENT; + if (*count == *capacity) { + const size_t next_capacity = *capacity == 0U ? 8U : *capacity * 2U; + if (next_capacity < *capacity) { + return SG_ERR_ALLOC; + } + size_t bytes = 0U; + if (!sg_size_multiply(next_capacity, sizeof(**values), &bytes)) { + return SG_ERR_ALLOC; + } + char **next = realloc(*values, bytes); + if (next == NULL) { + return SG_ERR_ALLOC; + } + *values = next; + *capacity = next_capacity; } - sg_dfa_builder *created = calloc(1U, sizeof(*created)); - if (created == NULL) { + char *copy = sg_string_duplicate(value); + if (copy == NULL) { return SG_ERR_ALLOC; } - *builder = created; + (*values)[*count] = copy; + ++*count; return SG_OK; } -void sg_dfa_builder_free(sg_dfa_builder *builder) { +sg_status sg_automaton_builder_add_state(sg_automaton_builder *builder, const char *state_key) { if (builder == NULL) { - return; + return SG_ERR_INVALID_ARGUMENT; } - sg_string_vec_free(&builder->states); - sg_string_vec_free(&builder->letters); - sg_transition_vec_free(&builder->transitions); - free(builder); + return sg_string_array_add(&builder->states, &builder->state_count, &builder->state_capacity, + state_key); } -sg_status sg_dfa_builder_add_state(sg_dfa_builder *builder, const char *state_key) { +sg_status sg_automaton_builder_add_action(sg_automaton_builder *builder, const char *action_key) { if (builder == NULL) { return SG_ERR_INVALID_ARGUMENT; } - return sg_string_vec_add_unique(&builder->states, state_key, NULL); + return sg_string_array_add(&builder->actions, &builder->action_count, &builder->action_capacity, + action_key); } -sg_status sg_dfa_builder_add_letter(sg_dfa_builder *builder, const char *letter) { +sg_status sg_automaton_builder_add_output(sg_automaton_builder *builder, const char *output_key) { if (builder == NULL) { return SG_ERR_INVALID_ARGUMENT; } - return sg_string_vec_add_unique(&builder->letters, letter, NULL); + return sg_string_array_add(&builder->outputs, &builder->output_count, &builder->output_capacity, + output_key); } -sg_status sg_dfa_builder_add_transition(sg_dfa_builder *builder, const char *source_key, - const char *letter, const char *target_key) { - if (builder == NULL || source_key == NULL || letter == NULL || target_key == NULL) { +static sg_status sg_builder_entry_add(sg_builder_entry **entries, size_t *count, size_t *capacity, + const char *source, const char *action, const char *value) { + if (entries == NULL || count == NULL || capacity == NULL || source == NULL || action == NULL || + value == NULL || source[0] == '\0' || action[0] == '\0' || value[0] == '\0') { return SG_ERR_INVALID_ARGUMENT; } - sg_status status = - sg_grow((void **)&builder->transitions.items, sizeof(builder->transitions.items[0]), - &builder->transitions.capacity, builder->transitions.length + 1U); - if (status != SG_OK) { - return status; + if (*count == *capacity) { + const size_t next_capacity = *capacity == 0U ? 16U : *capacity * 2U; + if (next_capacity < *capacity) { + return SG_ERR_ALLOC; + } + size_t bytes = 0U; + if (!sg_size_multiply(next_capacity, sizeof(**entries), &bytes)) { + return SG_ERR_ALLOC; + } + sg_builder_entry *next = realloc(*entries, bytes); + if (next == NULL) { + return SG_ERR_ALLOC; + } + *entries = next; + *capacity = next_capacity; } - - sg_transition_record record = { - .source = sg_strdup_owned(source_key), - .letter = sg_strdup_owned(letter), - .target = sg_strdup_owned(target_key), + sg_builder_entry entry = { + .source = sg_string_duplicate(source), + .action = sg_string_duplicate(action), + .value = sg_string_duplicate(value), }; - if (record.source == NULL || record.letter == NULL || record.target == NULL) { - sg_transition_record_free(&record); + if (entry.source == NULL || entry.action == NULL || entry.value == NULL) { + free(entry.source); + free(entry.action); + free(entry.value); return SG_ERR_ALLOC; } - - builder->transitions.items[builder->transitions.length] = record; - ++builder->transitions.length; + (*entries)[*count] = entry; + ++*count; return SG_OK; } -static sg_status sg_dfa_alloc_from_builder(const sg_dfa_builder *builder, bool needs_sink, - sg_dfa **dfa) { - sg_dfa *created = calloc(1U, sizeof(*created)); - if (created == NULL) { - return SG_ERR_ALLOC; +sg_status sg_automaton_builder_add_transition(sg_automaton_builder *builder, const char *source_key, + const char *action_key, const char *target_key) { + if (builder == NULL) { + return SG_ERR_INVALID_ARGUMENT; } + return sg_builder_entry_add(&builder->transitions, &builder->transition_count, + &builder->transition_capacity, source_key, action_key, target_key); +} - created->state_count = builder->states.length + (needs_sink ? 1U : 0U); - created->letter_count = builder->letters.length; - - created->states = (char **)calloc(created->state_count, sizeof(created->states[0])); - created->letters = (char **)calloc(created->letter_count, sizeof(created->letters[0])); - if (created->states == NULL || created->letters == NULL) { - free((void *)created->states); - free((void *)created->letters); - free(created); - return SG_ERR_ALLOC; +sg_status sg_automaton_builder_add_observation(sg_automaton_builder *builder, + const char *source_key, const char *action_key, + const char *output_key) { + if (builder == NULL) { + return SG_ERR_INVALID_ARGUMENT; } + return sg_builder_entry_add(&builder->observations, &builder->observation_count, + &builder->observation_capacity, source_key, action_key, output_key); +} - for (size_t i = 0U; i < builder->states.length; ++i) { - created->states[i] = sg_strdup_owned(builder->states.items[i]); - if (created->states[i] == NULL) { - sg_dfa_free(created); - return SG_ERR_ALLOC; - } +static sg_status sg_copy_keys(char *const *source, size_t count, char ***destination) { + char **keys = calloc(count, sizeof(*keys)); + if (keys == NULL) { + return SG_ERR_ALLOC; } - if (needs_sink) { - created->states[created->state_count - 1U] = sg_strdup_owned("__sink"); - if (created->states[created->state_count - 1U] == NULL) { - sg_dfa_free(created); + for (size_t index = 0U; index < count; ++index) { + keys[index] = sg_string_duplicate(source[index]); + if (keys[index] == NULL) { + sg_string_array_free(keys, count); return SG_ERR_ALLOC; } } + *destination = keys; + return SG_OK; +} - for (size_t i = 0U; i < builder->letters.length; ++i) { - created->letters[i] = sg_strdup_owned(builder->letters.items[i]); - if (created->letters[i] == NULL) { - sg_dfa_free(created); - return SG_ERR_ALLOC; +static sg_status sg_resolve_entries(const sg_automaton_builder *builder, + const sg_builder_entry *entries, size_t entry_count, + char *const *value_keys, size_t value_count, size_t *table) { + for (size_t index = 0U; index < entry_count; ++index) { + const size_t source = + sg_string_array_find(builder->states, builder->state_count, entries[index].source); + const size_t action = + sg_string_array_find(builder->actions, builder->action_count, entries[index].action); + const size_t value = sg_string_array_find(value_keys, value_count, entries[index].value); + if (source == SG_INDEX_NONE || action == SG_INDEX_NONE || value == SG_INDEX_NONE) { + return SG_ERR_NOT_FOUND; + } + const size_t cell = (source * builder->action_count) + action; + if (table[cell] != SG_INDEX_NONE) { + return table[cell] == value ? SG_ERR_DUPLICATE : SG_ERR_NONDETERMINISTIC; } + table[cell] = value; } + return SG_OK; +} - size_t transition_count = 0U; - sg_status status = sg_checked_mul(created->state_count, created->letter_count, &transition_count); - if (status != SG_OK) { - sg_dfa_free(created); - return status; +static sg_status sg_automaton_allocate(const sg_automaton_builder *builder, uint64_t generation, + sg_automaton **automaton) { + sg_automaton *created = calloc(1U, sizeof(*created)); + if (created == NULL) { + return SG_ERR_ALLOC; } - created->transitions = malloc(transition_count * sizeof(created->transitions[0])); - if (created->transitions == NULL && transition_count != 0U) { - sg_dfa_free(created); + created->generation = generation; + created->state_count = builder->state_count; + created->action_count = builder->action_count; + created->output_count = builder->output_count; + size_t cells = 0U; + if (!sg_size_multiply(created->state_count, created->action_count, &cells)) { + sg_automaton_free(created); return SG_ERR_ALLOC; } - for (size_t i = 0U; i < transition_count; ++i) { - created->transitions[i] = SG_INVALID; + created->transitions = malloc(cells * sizeof(*created->transitions)); + created->observations = malloc(cells * sizeof(*created->observations)); + if (created->transitions == NULL || created->observations == NULL) { + sg_automaton_free(created); + return SG_ERR_ALLOC; } - - *dfa = created; + for (size_t cell = 0U; cell < cells; ++cell) { + created->transitions[cell] = SG_INDEX_NONE; + created->observations[cell] = SG_INDEX_NONE; + } + *automaton = created; return SG_OK; } -sg_status sg_dfa_builder_build(sg_dfa_builder *builder, bool complete_with_sink, sg_dfa **dfa) { - if (builder == NULL || dfa == NULL) { +sg_status sg_automaton_builder_build(sg_automaton_builder *builder, uint64_t generation, + sg_automaton **automaton) { + if (builder == NULL || automaton == NULL) { return SG_ERR_INVALID_ARGUMENT; } - *dfa = NULL; - if (builder->states.length == 0U || builder->letters.length == 0U) { + *automaton = NULL; + if (builder->state_count == 0U || builder->action_count == 0U || builder->output_count == 0U) { return SG_ERR_INCOMPLETE; } - - bool needs_sink = false; - sg_status status = sg_dfa_alloc_from_builder(builder, false, dfa); + sg_automaton *created = NULL; + sg_status status = sg_automaton_allocate(builder, generation, &created); if (status != SG_OK) { return status; } - - for (size_t i = 0U; i < builder->transitions.length; ++i) { - const sg_transition_record *record = &builder->transitions.items[i]; - size_t source = 0U; - size_t letter = 0U; - size_t target = 0U; - if (sg_dfa_find_state(*dfa, record->source, &source) != SG_OK || - sg_dfa_find_letter(*dfa, record->letter, &letter) != SG_OK || - sg_dfa_find_state(*dfa, record->target, &target) != SG_OK) { - sg_dfa_free(*dfa); - *dfa = NULL; - return SG_ERR_NOT_FOUND; - } - size_t index = (source * (*dfa)->letter_count) + letter; - if ((*dfa)->transitions[index] != SG_INVALID) { - sg_dfa_free(*dfa); - *dfa = NULL; - return SG_ERR_NONDETERMINISTIC; - } - (*dfa)->transitions[index] = target; + status = sg_copy_keys(builder->states, builder->state_count, &created->state_keys); + if (status == SG_OK) { + status = sg_copy_keys(builder->actions, builder->action_count, &created->action_keys); } - - const size_t raw_transition_count = sg_dfa_transition_count(*dfa); - for (size_t i = 0U; i < raw_transition_count; ++i) { - if ((*dfa)->transitions[i] == SG_INVALID) { - needs_sink = true; - break; - } + if (status == SG_OK) { + status = sg_copy_keys(builder->outputs, builder->output_count, &created->output_keys); } - - if (!needs_sink) { - return SG_OK; + if (status == SG_OK) { + status = sg_resolve_entries(builder, builder->transitions, builder->transition_count, + builder->states, builder->state_count, created->transitions); } - if (!complete_with_sink) { - sg_dfa_free(*dfa); - *dfa = NULL; - return SG_ERR_INCOMPLETE; + if (status == SG_OK) { + status = sg_resolve_entries(builder, builder->observations, builder->observation_count, + builder->outputs, builder->output_count, created->observations); + } + size_t cells = 0U; + (void)sg_size_multiply(created->state_count, created->action_count, &cells); + for (size_t cell = 0U; status == SG_OK && cell < cells; ++cell) { + if (created->transitions[cell] == SG_INDEX_NONE || + created->observations[cell] == SG_INDEX_NONE) { + status = SG_ERR_INCOMPLETE; + } } - - sg_dfa_free(*dfa); - *dfa = NULL; - status = sg_dfa_alloc_from_builder(builder, true, dfa); if (status != SG_OK) { + sg_automaton_free(created); return status; } - - const size_t sink = (*dfa)->state_count - 1U; - const size_t transition_count = sg_dfa_transition_count(*dfa); - for (size_t i = 0U; i < transition_count; ++i) { - (*dfa)->transitions[i] = sink; - } - for (size_t i = 0U; i < builder->transitions.length; ++i) { - const sg_transition_record *record = &builder->transitions.items[i]; - size_t source = 0U; - size_t letter = 0U; - size_t target = 0U; - (void)sg_dfa_find_state(*dfa, record->source, &source); - (void)sg_dfa_find_letter(*dfa, record->letter, &letter); - (void)sg_dfa_find_state(*dfa, record->target, &target); - const size_t index = (source * (*dfa)->letter_count) + letter; - if ((*dfa)->transitions[index] != sink) { - sg_dfa_free(*dfa); - *dfa = NULL; - return SG_ERR_NONDETERMINISTIC; - } - (*dfa)->transitions[index] = target; - } + *automaton = created; return SG_OK; } -void sg_dfa_free(sg_dfa *dfa) { - if (dfa == NULL) { +void sg_automaton_free(sg_automaton *automaton) { + if (automaton == NULL) { return; } - if (dfa->states != NULL) { - for (size_t i = 0U; i < dfa->state_count; ++i) { - free(dfa->states[i]); - } - } - if (dfa->letters != NULL) { - for (size_t i = 0U; i < dfa->letter_count; ++i) { - free(dfa->letters[i]); - } - } - free((void *)dfa->states); - free((void *)dfa->letters); - free(dfa->transitions); - free(dfa); + sg_string_array_free(automaton->state_keys, automaton->state_count); + sg_string_array_free(automaton->action_keys, automaton->action_count); + sg_string_array_free(automaton->output_keys, automaton->output_count); + free(automaton->transitions); + free(automaton->observations); + free(automaton); } -size_t sg_dfa_state_count(const sg_dfa *dfa) { - return (dfa == NULL) ? 0U : dfa->state_count; +uint64_t sg_automaton_generation(const sg_automaton *automaton) { + return automaton == NULL ? 0U : automaton->generation; } -size_t sg_dfa_letter_count(const sg_dfa *dfa) { - return (dfa == NULL) ? 0U : dfa->letter_count; +size_t sg_automaton_state_count(const sg_automaton *automaton) { + return automaton == NULL ? 0U : automaton->state_count; } -size_t sg_dfa_transition_count(const sg_dfa *dfa) { - if (dfa == NULL) { - return 0U; - } - return dfa->state_count * dfa->letter_count; +size_t sg_automaton_action_count(const sg_automaton *automaton) { + return automaton == NULL ? 0U : automaton->action_count; } -const char *sg_dfa_state_key(const sg_dfa *dfa, size_t state) { - if (dfa == NULL || state >= dfa->state_count) { - return NULL; - } - return dfa->states[state]; +size_t sg_automaton_output_count(const sg_automaton *automaton) { + return automaton == NULL ? 0U : automaton->output_count; } -const char *sg_dfa_letter_key(const sg_dfa *dfa, size_t letter) { - if (dfa == NULL || letter >= dfa->letter_count) { - return NULL; +size_t sg_automaton_transition_count(const sg_automaton *automaton) { + if (automaton == NULL) { + return 0U; } - return dfa->letters[letter]; + return automaton->state_count * automaton->action_count; } -size_t sg_dfa_transition(const sg_dfa *dfa, size_t state, size_t letter) { - if (dfa == NULL || state >= dfa->state_count || letter >= dfa->letter_count) { - return SG_INVALID; - } - return dfa->transitions[(state * dfa->letter_count) + letter]; +const char *sg_automaton_state_key(const sg_automaton *automaton, size_t state) { + return automaton == NULL || state >= automaton->state_count ? NULL : automaton->state_keys[state]; } -sg_status sg_dfa_find_state(const sg_dfa *dfa, const char *state_key, size_t *state) { - if (dfa == NULL || state_key == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - for (size_t i = 0U; i < dfa->state_count; ++i) { - if (strcmp(dfa->states[i], state_key) == 0) { - if (state != NULL) { - *state = i; - } - return SG_OK; - } - } - return SG_ERR_NOT_FOUND; +const char *sg_automaton_action_key(const sg_automaton *automaton, size_t action) { + return automaton == NULL || action >= automaton->action_count ? NULL + : automaton->action_keys[action]; } -sg_status sg_dfa_find_letter(const sg_dfa *dfa, const char *letter, size_t *letter_id) { - if (dfa == NULL || letter == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - for (size_t i = 0U; i < dfa->letter_count; ++i) { - if (strcmp(dfa->letters[i], letter) == 0) { - if (letter_id != NULL) { - *letter_id = i; - } - return SG_OK; - } - } - return SG_ERR_NOT_FOUND; +const char *sg_automaton_output_key(const sg_automaton *automaton, size_t output) { + return automaton == NULL || output >= automaton->output_count ? NULL + : automaton->output_keys[output]; } -static size_t sg_pair_index(size_t state_count, size_t first, size_t second) { - size_t low = first; - size_t high = second; - if (low > high) { - low = second; - high = first; +size_t sg_automaton_transition(const sg_automaton *automaton, size_t state, size_t action) { + if (automaton == NULL || state >= automaton->state_count || action >= automaton->action_count) { + return SG_INDEX_NONE; } - return (low * state_count) - ((low * (low - 1U)) / 2U) + (high - low); + return automaton->transitions[(state * automaton->action_count) + action]; } -static void sg_pair_from_index(size_t state_count, size_t index, size_t *first, size_t *second) { - size_t row_start = 0U; - for (size_t low = 0U; low < state_count; ++low) { - const size_t row_len = state_count - low; - if (index < row_start + row_len) { - *first = low; - *second = low + (index - row_start); - return; - } - row_start += row_len; +size_t sg_automaton_observation(const sg_automaton *automaton, size_t state, size_t action) { + if (automaton == NULL || state >= automaton->state_count || action >= automaton->action_count) { + return SG_INDEX_NONE; } - *first = SG_INVALID; - *second = SG_INVALID; + return automaton->observations[(state * automaton->action_count) + action]; } -sg_status sg_pair_oracle_build(const sg_dfa *dfa, sg_pair_oracle **oracle) { - if (dfa == NULL || oracle == NULL) { +static sg_status sg_find_key(char *const *keys, size_t count, const char *key, size_t *identifier) { + if (keys == NULL || key == NULL || identifier == NULL) { return SG_ERR_INVALID_ARGUMENT; } - if (dfa->state_count == 0U || dfa->letter_count == 0U) { - return SG_ERR_INVALID_ARGUMENT; - } - *oracle = NULL; - - sg_pair_oracle *created = calloc(1U, sizeof(*created)); - if (created == NULL) { - return SG_ERR_ALLOC; - } - created->dfa = dfa; - created->pair_count = (dfa->state_count * (dfa->state_count + 1U)) / 2U; - - sg_status status = sg_checked_mul(created->pair_count, dfa->letter_count, &created->edge_count); - if (status != SG_OK) { - sg_pair_oracle_free(created); - return status; - } - - const size_t edge_slots = (created->edge_count == 0U) ? 1U : created->edge_count; - created->forward = malloc(edge_slots * sizeof(created->forward[0])); - created->reverse_next = malloc(edge_slots * sizeof(created->reverse_next[0])); - created->reverse_from = malloc(edge_slots * sizeof(created->reverse_from[0])); - created->reverse_letter = malloc(edge_slots * sizeof(created->reverse_letter[0])); - created->reverse_head = malloc(created->pair_count * sizeof(created->reverse_head[0])); - created->dist = malloc(created->pair_count * sizeof(created->dist[0])); - created->next_pair = malloc(created->pair_count * sizeof(created->next_pair[0])); - created->witness = malloc(created->pair_count * sizeof(created->witness[0])); - if (created->forward == NULL || created->reverse_next == NULL || created->reverse_from == NULL || - created->reverse_letter == NULL || created->reverse_head == NULL || created->dist == NULL || - created->next_pair == NULL || created->witness == NULL) { - sg_pair_oracle_free(created); - return SG_ERR_ALLOC; - } - - for (size_t i = 0U; i < created->pair_count; ++i) { - created->reverse_head[i] = SG_INVALID; - created->dist[i] = SG_INVALID; - created->next_pair[i] = SG_INVALID; - created->witness[i] = SG_INVALID; - } - - for (size_t pair = 0U; pair < created->pair_count; ++pair) { - size_t first = 0U; - size_t second = 0U; - sg_pair_from_index(dfa->state_count, pair, &first, &second); - for (size_t letter = 0U; letter < dfa->letter_count; ++letter) { - const size_t image_first = sg_dfa_transition(dfa, first, letter); - const size_t image_second = sg_dfa_transition(dfa, second, letter); - const size_t image_pair = sg_pair_index(dfa->state_count, image_first, image_second); - const size_t edge = (pair * dfa->letter_count) + letter; - created->forward[edge] = image_pair; - created->reverse_from[edge] = pair; - created->reverse_letter[edge] = letter; - created->reverse_next[edge] = created->reverse_head[image_pair]; - created->reverse_head[image_pair] = edge; - } - } - - size_t *queue = malloc(created->pair_count * sizeof(queue[0])); - if (queue == NULL && created->pair_count != 0U) { - sg_pair_oracle_free(created); - return SG_ERR_ALLOC; - } - - size_t head = 0U; - size_t tail = 0U; - for (size_t state = 0U; state < dfa->state_count; ++state) { - const size_t diagonal = sg_pair_index(dfa->state_count, state, state); - created->dist[diagonal] = 0U; - queue[tail] = diagonal; - ++tail; - } - - while (head < tail) { - const size_t image_pair = queue[head]; - ++head; - for (size_t edge = created->reverse_head[image_pair]; edge != SG_INVALID; - edge = created->reverse_next[edge]) { - const size_t source_pair = created->reverse_from[edge]; - if (created->dist[source_pair] == SG_INVALID) { - created->dist[source_pair] = created->dist[image_pair] + 1U; - created->next_pair[source_pair] = image_pair; - created->witness[source_pair] = created->reverse_letter[edge]; - queue[tail] = source_pair; - ++tail; - } - } + const size_t found = sg_string_array_find(keys, count, key); + if (found == SG_INDEX_NONE) { + return SG_ERR_NOT_FOUND; } + *identifier = found; + return SG_OK; +} - free(queue); +sg_status sg_automaton_find_state(const sg_automaton *automaton, const char *state_key, + size_t *state) { + return automaton == NULL + ? SG_ERR_INVALID_ARGUMENT + : sg_find_key(automaton->state_keys, automaton->state_count, state_key, state); +} - for (size_t pair = 0U; pair < created->pair_count; ++pair) { - if (created->dist[pair] != SG_INVALID) { - ++created->mergeable_pairs; - } - } - *oracle = created; - return SG_OK; -} - -void sg_pair_oracle_free(sg_pair_oracle *oracle) { - if (oracle == NULL) { - return; - } - free(oracle->forward); - free(oracle->reverse_head); - free(oracle->reverse_next); - free(oracle->reverse_from); - free(oracle->reverse_letter); - free(oracle->dist); - free(oracle->next_pair); - free(oracle->witness); - free(oracle); -} - -size_t sg_pair_oracle_pair_count(const sg_pair_oracle *oracle) { - return (oracle == NULL) ? 0U : oracle->pair_count; -} - -size_t sg_pair_oracle_pair_edge_count(const sg_pair_oracle *oracle) { - return (oracle == NULL) ? 0U : oracle->edge_count; -} - -size_t sg_pair_oracle_mergeable_pair_count(const sg_pair_oracle *oracle) { - return (oracle == NULL) ? 0U : oracle->mergeable_pairs; -} - -sg_status sg_pair_oracle_pair_states(const sg_pair_oracle *oracle, size_t pair, size_t *first, - size_t *second) { - if (oracle == NULL || oracle->dfa == NULL || first == NULL || second == NULL || - pair >= oracle->pair_count) { - return SG_ERR_INVALID_ARGUMENT; - } - sg_pair_from_index(oracle->dfa->state_count, pair, first, second); - if (*first == SG_INVALID || *second == SG_INVALID) { - return SG_ERR_INVALID_ARGUMENT; - } - return SG_OK; -} - -sg_status sg_pair_oracle_pair_next(const sg_pair_oracle *oracle, size_t pair, size_t letter, - size_t *next_pair) { - if (oracle == NULL || oracle->dfa == NULL || next_pair == NULL || pair >= oracle->pair_count || - letter >= oracle->dfa->letter_count) { - return SG_ERR_INVALID_ARGUMENT; - } - *next_pair = oracle->forward[(pair * oracle->dfa->letter_count) + letter]; - return SG_OK; -} - -sg_status sg_pair_oracle_pair_witness(const sg_pair_oracle *oracle, size_t pair, bool *has_witness, - size_t *distance, size_t *letter, size_t *next_pair) { - if (oracle == NULL || has_witness == NULL || distance == NULL || letter == NULL || - next_pair == NULL || pair >= oracle->pair_count) { - return SG_ERR_INVALID_ARGUMENT; - } - *has_witness = oracle->dist[pair] != SG_INVALID; - *distance = oracle->dist[pair]; - *letter = oracle->witness[pair]; - *next_pair = oracle->next_pair[pair]; - return SG_OK; -} - -bool sg_pair_oracle_has_witness(const sg_pair_oracle *oracle, size_t first, size_t second) { - if (oracle == NULL || oracle->dfa == NULL || first >= oracle->dfa->state_count || - second >= oracle->dfa->state_count) { - return false; - } - const size_t pair = sg_pair_index(oracle->dfa->state_count, first, second); - return oracle->dist[pair] != SG_INVALID; -} - -sg_status sg_pair_oracle_witness_word(const sg_pair_oracle *oracle, size_t first, size_t second, - sg_word *word) { - if (oracle == NULL || word == NULL || oracle->dfa == NULL || first >= oracle->dfa->state_count || - second >= oracle->dfa->state_count) { - return SG_ERR_INVALID_ARGUMENT; - } - sg_status status = sg_word_init(word); - if (status != SG_OK) { - return status; - } - size_t pair = sg_pair_index(oracle->dfa->state_count, first, second); - if (oracle->dist[pair] == SG_INVALID) { - return SG_ERR_UNSYNCHRONIZABLE; - } - while (oracle->dist[pair] != 0U) { - status = sg_word_append(word, oracle->witness[pair]); - if (status != SG_OK) { - sg_word_free(word); - return status; - } - pair = oracle->next_pair[pair]; - } - return SG_OK; -} - -static size_t sg_bitset_word_count(size_t bits) { - return (bits + (size_t)SG_BITS_PER_WORD - 1U) / (size_t)SG_BITS_PER_WORD; -} - -static sg_status sg_bitset_init(sg_bitset *set, size_t bit_count) { - if (set == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - set->word_count = sg_bitset_word_count(bit_count); - set->words = calloc(set->word_count, sizeof(set->words[0])); - if (set->words == NULL && set->word_count != 0U) { - return SG_ERR_ALLOC; - } - return SG_OK; -} - -static void sg_bitset_free(sg_bitset *set) { - if (set == NULL) { - return; - } - free(set->words); - set->words = NULL; - set->word_count = 0U; -} - -static void sg_bitset_clear(sg_bitset *set) { - if (set == NULL) { - return; - } - memset(set->words, 0, set->word_count * sizeof(set->words[0])); -} - -static void sg_bitset_set(sg_bitset *set, size_t bit) { - set->words[bit / (size_t)SG_BITS_PER_WORD] |= UINT64_C(1) << (bit % (size_t)SG_BITS_PER_WORD); -} - -static bool sg_bitset_has(const sg_bitset *set, size_t bit) { - return (set->words[bit / (size_t)SG_BITS_PER_WORD] & - (UINT64_C(1) << (bit % (size_t)SG_BITS_PER_WORD))) != 0U; -} - -static size_t sg_bitset_count(const sg_bitset *set) { - size_t count = 0U; - for (size_t i = 0U; i < set->word_count; ++i) { - count += (size_t)__builtin_popcountll(set->words[i]); - } - return count; -} - -static bool sg_bitset_equal(const sg_bitset *lhs, const sg_bitset *rhs) { - if (lhs->word_count != rhs->word_count) { - return false; - } - return memcmp(lhs->words, rhs->words, lhs->word_count * sizeof(lhs->words[0])) == 0; -} - -static bool sg_bitset_subset_of(const sg_bitset *small, const sg_bitset *large) { - if (small->word_count != large->word_count) { - return false; - } - for (size_t i = 0U; i < small->word_count; ++i) { - if ((small->words[i] & ~large->words[i]) != 0U) { - return false; - } - } - return true; -} - -static sg_status sg_bitset_copy(const sg_bitset *src, sg_bitset *dst) { - if (src == NULL || dst == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - dst->word_count = src->word_count; - dst->words = malloc(dst->word_count * sizeof(dst->words[0])); - if (dst->words == NULL && dst->word_count != 0U) { - return SG_ERR_ALLOC; - } - memcpy(dst->words, src->words, dst->word_count * sizeof(dst->words[0])); - return SG_OK; -} - -static sg_status sg_bitset_from_ids(size_t state_count, const size_t *ids, size_t id_count, - sg_bitset *set) { - sg_status status = sg_bitset_init(set, state_count); - if (status != SG_OK) { - return status; - } - for (size_t i = 0U; i < id_count; ++i) { - if (ids[i] >= state_count) { - sg_bitset_free(set); - return SG_ERR_INVALID_ARGUMENT; - } - sg_bitset_set(set, ids[i]); - } - return SG_OK; -} - -static sg_status sg_ids_from_bitset(const sg_bitset *set, size_t state_count, size_t *ids, - size_t *id_count) { - if (set == NULL || ids == NULL || id_count == NULL) { - return SG_ERR_INVALID_ARGUMENT; - } - size_t written = 0U; - for (size_t state = 0U; state < state_count; ++state) { - if (sg_bitset_has(set, state)) { - ids[written] = state; - ++written; - } - } - *id_count = written; - return SG_OK; -} - -static sg_status sg_visit_cache_node(const sg_dfa *dfa, const sg_bitset *set, const sg_word *word, - sg_cache_visitor visitor, void *ctx) { - if (visitor == NULL) { - return SG_OK; - } - size_t *states = malloc(dfa->state_count * sizeof(states[0])); - size_t state_count = 0U; - if (states == NULL && dfa->state_count != 0U) { - return SG_ERR_ALLOC; - } - sg_status status = sg_ids_from_bitset(set, dfa->state_count, states, &state_count); - if (status == SG_OK) { - status = visitor(ctx, states, state_count, word); - } - free(states); - return status; -} - -static sg_status sg_apply_letter_to_bitset(const sg_dfa *dfa, const sg_bitset *input, size_t letter, - sg_bitset *output) { - sg_bitset_clear(output); - for (size_t state = 0U; state < dfa->state_count; ++state) { - if (sg_bitset_has(input, state)) { - sg_bitset_set(output, sg_dfa_transition(dfa, state, letter)); - } - } - return SG_OK; -} - -static sg_status sg_apply_word_to_bitset(const sg_dfa *dfa, const sg_bitset *initial, - const sg_word *word, sg_bitset *output) { - sg_bitset current = {0}; - sg_bitset next = {0}; - sg_status status = sg_bitset_copy(initial, ¤t); - if (status != SG_OK) { - return status; - } - status = sg_bitset_init(&next, dfa->state_count); - if (status != SG_OK) { - sg_bitset_free(¤t); - return status; - } - for (size_t i = 0U; i < word->length; ++i) { - (void)sg_apply_letter_to_bitset(dfa, ¤t, word->letters[i], &next); - sg_bitset temp = current; - current = next; - next = temp; - } - sg_bitset_free(&next); - *output = current; - return SG_OK; -} - -static bool sg_bitset_intersects_ids(const sg_bitset *set, const size_t *ids, size_t id_count) { - for (size_t i = 0U; i < id_count; ++i) { - if (sg_bitset_has(set, ids[i])) { - return true; - } - } - return false; -} - -static bool sg_target_condition_holds(const sg_bitset *final, const size_t *targets, - size_t target_count, sg_mode mode) { - const size_t count = sg_bitset_count(final); - if (mode == SG_MODE_SYNC) { - return count <= 1U; - } - if (target_count == 0U) { - return false; - } - for (size_t word = 0U; word < final->word_count; ++word) { - uint64_t remaining = final->words[word]; - while (remaining != 0U) { - const size_t offset = (size_t)__builtin_ctzll(remaining); - const size_t state = (word * (size_t)SG_BITS_PER_WORD) + offset; - bool found = false; - for (size_t i = 0U; i < target_count; ++i) { - if (targets[i] == state) { - found = true; - break; - } - } - if (!found) { - return false; - } - remaining &= remaining - 1U; - } - } - if (mode == SG_MODE_REACH) { - return true; - } - return count == 1U && sg_bitset_intersects_ids(final, targets, target_count); -} - -static sg_status sg_subset_vec_push(sg_subset_vec *vec, sg_bitset *set, sg_word *word) { - sg_status status = - sg_grow((void **)&vec->items, sizeof(vec->items[0]), &vec->capacity, vec->length + 1U); - if (status != SG_OK) { - return status; - } - vec->items[vec->length].set = *set; - vec->items[vec->length].word = *word; - ++vec->length; - set->words = NULL; - set->word_count = 0U; - word->letters = NULL; - word->length = 0U; - word->capacity = 0U; - return SG_OK; -} - -static void sg_subset_vec_free(sg_subset_vec *vec) { - if (vec == NULL) { - return; - } - for (size_t i = 0U; i < vec->length; ++i) { - sg_bitset_free(&vec->items[i].set); - sg_word_free(&vec->items[i].word); - } - free(vec->items); - vec->items = NULL; - vec->length = 0U; - vec->capacity = 0U; -} - -static bool sg_subset_vec_prunes(const sg_subset_vec *vec, const sg_bitset *candidate) { - for (size_t i = 0U; i < vec->length; ++i) { - if (sg_bitset_equal(&vec->items[i].set, candidate) || - sg_bitset_subset_of(candidate, &vec->items[i].set)) { - return true; - } - } - return false; -} - -static sg_status sg_preimage(const sg_dfa *dfa, const sg_bitset *target, size_t letter, - sg_bitset *preimage) { - sg_status status = sg_bitset_init(preimage, dfa->state_count); - if (status != SG_OK) { - return status; - } - for (size_t state = 0U; state < dfa->state_count; ++state) { - const size_t image = sg_dfa_transition(dfa, state, letter); - if (sg_bitset_has(target, image)) { - sg_bitset_set(preimage, state); - } - } - return SG_OK; -} - -static sg_status sg_seed_reverse_queue(const sg_dfa *dfa, const size_t *targets, - size_t target_count, sg_mode mode, sg_subset_vec *queue) { - sg_status status = SG_OK; - if (mode == SG_MODE_REACH) { - sg_bitset seed = {0}; - sg_word seed_word = {0}; - status = sg_bitset_from_ids(dfa->state_count, targets, target_count, &seed); - if (status != SG_OK) { - return status; - } - status = sg_word_init(&seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - return status; - } - status = sg_subset_vec_push(queue, &seed, &seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - sg_word_free(&seed_word); - } - return status; - } - - const bool all_singletons = (mode == SG_MODE_SYNC && target_count == 0U); - const size_t seed_count = all_singletons ? dfa->state_count : target_count; - for (size_t i = 0U; i < seed_count; ++i) { - const size_t target = all_singletons ? i : targets[i]; - sg_bitset seed = {0}; - sg_word seed_word = {0}; - status = sg_bitset_init(&seed, dfa->state_count); - if (status != SG_OK) { - return status; - } - sg_bitset_set(&seed, target); - status = sg_word_init(&seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - return status; - } - status = sg_subset_vec_push(queue, &seed, &seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - sg_word_free(&seed_word); - return status; - } - } - return SG_OK; -} - -static sg_status sg_exact_reverse_search(const sg_dfa *dfa, const sg_bitset *initial, - const size_t *targets, size_t target_count, sg_mode mode, - size_t budget, sg_word *word) { - if (budget == 0U) { - return SG_ERR_RESOURCE_BOUND; - } - - sg_subset_vec queue = {0}; - sg_subset_vec antichain = {0}; - size_t head = 0U; - sg_status status = SG_OK; - - if (mode == SG_MODE_REACH) { - sg_bitset seed = {0}; - sg_word seed_word = {0}; - status = sg_bitset_from_ids(dfa->state_count, targets, target_count, &seed); - if (status != SG_OK) { - goto cleanup; - } - status = sg_word_init(&seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - goto cleanup; - } - status = sg_subset_vec_push(&queue, &seed, &seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - sg_word_free(&seed_word); - goto cleanup; - } - } else { - const bool all_singletons = (mode == SG_MODE_SYNC && target_count == 0U); - const size_t seed_count = all_singletons ? dfa->state_count : target_count; - for (size_t i = 0U; i < seed_count; ++i) { - const size_t target = all_singletons ? i : targets[i]; - sg_bitset seed = {0}; - sg_word seed_word = {0}; - status = sg_bitset_init(&seed, dfa->state_count); - if (status != SG_OK) { - goto cleanup; - } - sg_bitset_set(&seed, target); - status = sg_word_init(&seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - goto cleanup; - } - status = sg_subset_vec_push(&queue, &seed, &seed_word); - if (status != SG_OK) { - sg_bitset_free(&seed); - sg_word_free(&seed_word); - goto cleanup; - } - } - } - - while (head < queue.length && head < budget) { - sg_bitset current_set = {0}; - sg_word current_word = {0}; - status = sg_bitset_copy(&queue.items[head].set, ¤t_set); - if (status != SG_OK) { - goto cleanup; - } - status = sg_word_copy(&queue.items[head].word, ¤t_word); - if (status != SG_OK) { - sg_bitset_free(¤t_set); - goto cleanup; - } - - if (sg_bitset_subset_of(initial, ¤t_set)) { - status = sg_word_copy(¤t_word, word); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - - for (size_t letter = 0U; letter < dfa->letter_count; ++letter) { - sg_bitset preimage = {0}; - sg_word next_word = {0}; - status = sg_preimage(dfa, ¤t_set, letter, &preimage); - if (status != SG_OK) { - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - if (sg_subset_vec_prunes(&antichain, &preimage) || sg_subset_vec_prunes(&queue, &preimage)) { - sg_bitset_free(&preimage); - continue; - } - status = sg_word_copy(¤t_word, &next_word); - if (status != SG_OK) { - sg_bitset_free(&preimage); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_word_prepend(&next_word, letter); - if (status != SG_OK) { - sg_bitset_free(&preimage); - sg_word_free(&next_word); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_subset_vec_push(&queue, &preimage, &next_word); - if (status != SG_OK) { - sg_bitset_free(&preimage); - sg_word_free(&next_word); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - } - - sg_bitset antichain_set = {0}; - sg_word antichain_word = {0}; - status = sg_bitset_copy(¤t_set, &antichain_set); - if (status != SG_OK) { - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_word_copy(¤t_word, &antichain_word); - if (status != SG_OK) { - sg_bitset_free(&antichain_set); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_subset_vec_push(&antichain, &antichain_set, &antichain_word); - if (status != SG_OK) { - sg_bitset_free(&antichain_set); - sg_word_free(&antichain_word); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - ++head; - } - - status = SG_ERR_RESOURCE_BOUND; - -cleanup: - sg_subset_vec_free(&queue); - sg_subset_vec_free(&antichain); - return status; -} - -sg_status sg_expand_cache_visit(const sg_dfa *dfa, const size_t *targets, size_t target_count, - sg_mode mode, size_t budget, sg_cache_visitor visitor, void *ctx, - size_t *expanded, size_t *cache_size) { - if (dfa == NULL || expanded == NULL || cache_size == NULL || - (targets == NULL && target_count != 0U)) { - return SG_ERR_INVALID_ARGUMENT; - } - *expanded = 0U; - *cache_size = 0U; - if (budget == 0U) { - return SG_ERR_RESOURCE_BOUND; - } - - sg_subset_vec queue = {0}; - sg_subset_vec antichain = {0}; - sg_status status = SG_OK; - size_t head = 0U; - - status = sg_seed_reverse_queue(dfa, targets, target_count, mode, &queue); - if (status != SG_OK) { - goto cleanup; - } - - while (head < queue.length && *expanded < budget) { - sg_bitset current_set = {0}; - sg_word current_word = {0}; - status = sg_bitset_copy(&queue.items[head].set, ¤t_set); - if (status != SG_OK) { - goto cleanup; - } - status = sg_word_copy(&queue.items[head].word, ¤t_word); - if (status != SG_OK) { - sg_bitset_free(¤t_set); - goto cleanup; - } - - status = sg_visit_cache_node(dfa, ¤t_set, ¤t_word, visitor, ctx); - if (status != SG_OK) { - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - - for (size_t letter = 0U; letter < dfa->letter_count; ++letter) { - sg_bitset preimage = {0}; - sg_word next_word = {0}; - status = sg_preimage(dfa, ¤t_set, letter, &preimage); - if (status != SG_OK) { - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - if (sg_subset_vec_prunes(&antichain, &preimage) || sg_subset_vec_prunes(&queue, &preimage)) { - sg_bitset_free(&preimage); - continue; - } - status = sg_word_copy(¤t_word, &next_word); - if (status != SG_OK) { - sg_bitset_free(&preimage); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_word_prepend(&next_word, letter); - if (status != SG_OK) { - sg_bitset_free(&preimage); - sg_word_free(&next_word); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_subset_vec_push(&queue, &preimage, &next_word); - if (status != SG_OK) { - sg_bitset_free(&preimage); - sg_word_free(&next_word); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - } - - sg_bitset antichain_set = {0}; - sg_word antichain_word = {0}; - status = sg_bitset_copy(¤t_set, &antichain_set); - if (status != SG_OK) { - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_word_copy(¤t_word, &antichain_word); - if (status != SG_OK) { - sg_bitset_free(&antichain_set); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - status = sg_subset_vec_push(&antichain, &antichain_set, &antichain_word); - if (status != SG_OK) { - sg_bitset_free(&antichain_set); - sg_word_free(&antichain_word); - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - goto cleanup; - } - - sg_bitset_free(¤t_set); - sg_word_free(¤t_word); - ++head; - ++*expanded; - } - - *cache_size = queue.length; - status = (head < queue.length) ? SG_ERR_RESOURCE_BOUND : SG_OK; - -cleanup: - if (status != SG_OK && status != SG_ERR_RESOURCE_BOUND) { - *expanded = 0U; - *cache_size = 0U; - } else { - *cache_size = queue.length; - } - sg_subset_vec_free(&queue); - sg_subset_vec_free(&antichain); - return status; -} - -sg_status sg_expand_cache(const sg_dfa *dfa, const size_t *targets, size_t target_count, - sg_mode mode, size_t budget, size_t *expanded, size_t *cache_size) { - return sg_expand_cache_visit(dfa, targets, target_count, mode, budget, NULL, NULL, expanded, - cache_size); -} - -static sg_status sg_greedy_sync(const sg_dfa *dfa, const sg_pair_oracle *oracle, - const sg_bitset *initial, sg_word *word, sg_bitset *final) { - sg_status status = sg_word_init(word); - if (status != SG_OK) { - return status; - } - - sg_bitset active = {0}; - sg_bitset next = {0}; - status = sg_bitset_copy(initial, &active); - if (status != SG_OK) { - sg_word_free(word); - return status; - } - status = sg_bitset_init(&next, dfa->state_count); - if (status != SG_OK) { - sg_word_free(word); - sg_bitset_free(&active); - return status; - } - - while (sg_bitset_count(&active) > 1U) { - size_t best_first = SG_INVALID; - size_t best_second = SG_INVALID; - size_t best_dist = SG_INVALID; - - for (size_t first = 0U; first < dfa->state_count; ++first) { - if (!sg_bitset_has(&active, first)) { - continue; - } - for (size_t second = first + 1U; second < dfa->state_count; ++second) { - if (!sg_bitset_has(&active, second)) { - continue; - } - const size_t pair = sg_pair_index(dfa->state_count, first, second); - if (oracle->dist[pair] != SG_INVALID && oracle->dist[pair] < best_dist) { - best_dist = oracle->dist[pair]; - best_first = first; - best_second = second; - } - } - } - - if (best_first == SG_INVALID || best_second == SG_INVALID) { - sg_bitset_free(&active); - sg_bitset_free(&next); - sg_word_free(word); - return SG_ERR_UNSYNCHRONIZABLE; - } - - sg_word witness = {0}; - status = sg_pair_oracle_witness_word(oracle, best_first, best_second, &witness); - if (status != SG_OK) { - sg_bitset_free(&active); - sg_bitset_free(&next); - sg_word_free(word); - return status; - } - for (size_t i = 0U; i < witness.length; ++i) { - (void)sg_apply_letter_to_bitset(dfa, &active, witness.letters[i], &next); - sg_bitset temp = active; - active = next; - next = temp; - } - status = sg_word_extend(word, &witness); - sg_word_free(&witness); - if (status != SG_OK) { - sg_bitset_free(&active); - sg_bitset_free(&next); - sg_word_free(word); - return status; - } - } - - sg_bitset_free(&next); - *final = active; - return SG_OK; -} - -sg_status sg_word_for_set(const sg_dfa *dfa, const sg_pair_oracle *oracle, const size_t *initial, - size_t initial_count, const size_t *targets, size_t target_count, - sg_mode mode, size_t exact_budget, sg_word_result *result) { - if (dfa == NULL || oracle == NULL || result == NULL || (initial == NULL && initial_count != 0U) || - (targets == NULL && target_count != 0U)) { - return SG_ERR_INVALID_ARGUMENT; - } - result->kind = SG_RESULT_FAILURE; - result->status = SG_OK; - result->final_state = SG_INVALID; - result->final_count = 0U; - sg_status status = sg_word_init(&result->word); - if (status != SG_OK) { - return status; - } - - sg_bitset initial_set = {0}; - status = sg_bitset_from_ids(dfa->state_count, initial, initial_count, &initial_set); - if (status != SG_OK) { - sg_word_result_free(result); - return status; - } - - const size_t unique_initial_count = sg_bitset_count(&initial_set); - if (unique_initial_count <= 1U) { - result->kind = SG_RESULT_TRIVIAL; - result->status = SG_OK; - result->final_count = unique_initial_count; - if (unique_initial_count == 1U) { - size_t states[1] = {0U}; - size_t count = 0U; - (void)sg_ids_from_bitset(&initial_set, dfa->state_count, states, &count); - result->final_state = states[0]; - } - sg_bitset_free(&initial_set); - return SG_OK; - } - - if (mode == SG_MODE_REACH && - sg_target_condition_holds(&initial_set, targets, target_count, mode)) { - result->kind = SG_RESULT_TRIVIAL; - result->status = SG_OK; - result->final_count = unique_initial_count; - sg_bitset_free(&initial_set); - return SG_OK; - } - - sg_word greedy_word = {0}; - sg_bitset greedy_final = {0}; - status = sg_greedy_sync(dfa, oracle, &initial_set, &greedy_word, &greedy_final); - if (status == SG_OK) { - const bool target_ok = sg_target_condition_holds(&greedy_final, targets, target_count, mode); - if (mode == SG_MODE_SYNC || target_ok) { - result->kind = - (mode == SG_MODE_SYNC) ? SG_RESULT_PAIR_GREEDY : SG_RESULT_PAIR_GREEDY_TARGETED; - result->status = SG_OK; - result->word = greedy_word; - result->final_count = sg_bitset_count(&greedy_final); - if (result->final_count == 1U) { - size_t states[1] = {0U}; - size_t count = 0U; - (void)sg_ids_from_bitset(&greedy_final, dfa->state_count, states, &count); - result->final_state = states[0]; - } - sg_bitset_free(&greedy_final); - sg_bitset_free(&initial_set); - return SG_OK; - } - } - sg_word_free(&greedy_word); - sg_bitset_free(&greedy_final); - - sg_word exact_word = {0}; - status = sg_exact_reverse_search(dfa, &initial_set, targets, target_count, mode, exact_budget, - &exact_word); - if (status == SG_OK) { - sg_bitset exact_final = {0}; - status = sg_apply_word_to_bitset(dfa, &initial_set, &exact_word, &exact_final); - if (status != SG_OK) { - sg_word_free(&exact_word); - sg_bitset_free(&initial_set); - return status; - } - result->kind = SG_RESULT_EXACT_EXPANDED; - result->status = SG_OK; - result->word = exact_word; - result->final_count = sg_bitset_count(&exact_final); - if (result->final_count == 1U) { - size_t states[1] = {0U}; - size_t count = 0U; - (void)sg_ids_from_bitset(&exact_final, dfa->state_count, states, &count); - result->final_state = states[0]; - } - sg_bitset_free(&exact_final); - sg_bitset_free(&initial_set); - return SG_OK; - } - - result->kind = (status == SG_ERR_RESOURCE_BOUND) ? SG_RESULT_RESOURCE_BOUND : SG_RESULT_FAILURE; - result->status = status; - sg_bitset_free(&initial_set); - return status; -} - -void sg_word_result_free(sg_word_result *result) { - if (result == NULL) { - return; - } - sg_word_free(&result->word); - result->kind = SG_RESULT_FAILURE; - result->status = SG_OK; - result->final_state = SG_INVALID; - result->final_count = 0U; -} - -sg_status sg_apply_word_to_set(const sg_dfa *dfa, const size_t *initial, size_t initial_count, - const sg_word *word, size_t *output, size_t *output_count) { - if (dfa == NULL || word == NULL || output == NULL || output_count == NULL || - (initial == NULL && initial_count != 0U)) { - return SG_ERR_INVALID_ARGUMENT; - } - sg_bitset initial_set = {0}; - sg_status status = sg_bitset_from_ids(dfa->state_count, initial, initial_count, &initial_set); - if (status != SG_OK) { - return status; - } - sg_bitset final_set = {0}; - status = sg_apply_word_to_bitset(dfa, &initial_set, word, &final_set); - sg_bitset_free(&initial_set); - if (status != SG_OK) { - return status; - } - status = sg_ids_from_bitset(&final_set, dfa->state_count, output, output_count); - sg_bitset_free(&final_set); - return status; -} - -sg_status sg_explain_word(const sg_dfa *dfa, const size_t *initial, size_t initial_count, - const sg_word *word, size_t **steps, size_t **step_counts, - size_t *step_count) { - if (dfa == NULL || word == NULL || steps == NULL || step_counts == NULL || step_count == NULL || - (initial == NULL && initial_count != 0U)) { - return SG_ERR_INVALID_ARGUMENT; - } - *steps = NULL; - *step_counts = NULL; - *step_count = word->length + 1U; - - size_t *created_steps = calloc(*step_count * dfa->state_count, sizeof(created_steps[0])); - size_t *created_counts = calloc(*step_count, sizeof(created_counts[0])); - if ((created_steps == NULL && *step_count * dfa->state_count != 0U) || created_counts == NULL) { - free(created_steps); - free(created_counts); - return SG_ERR_ALLOC; - } - - sg_bitset active = {0}; - sg_bitset next = {0}; - sg_status status = sg_bitset_from_ids(dfa->state_count, initial, initial_count, &active); - if (status != SG_OK) { - free(created_steps); - free(created_counts); - return status; - } - status = sg_bitset_init(&next, dfa->state_count); - if (status != SG_OK) { - free(created_steps); - free(created_counts); - sg_bitset_free(&active); - return status; - } - - for (size_t step = 0U; step < *step_count; ++step) { - status = sg_ids_from_bitset(&active, dfa->state_count, &created_steps[step * dfa->state_count], - &created_counts[step]); - if (status != SG_OK) { - free(created_steps); - free(created_counts); - sg_bitset_free(&active); - sg_bitset_free(&next); - return status; - } - if (step < word->length) { - (void)sg_apply_letter_to_bitset(dfa, &active, word->letters[step], &next); - sg_bitset temp = active; - active = next; - next = temp; - } - } - - sg_bitset_free(&active); - sg_bitset_free(&next); - *steps = created_steps; - *step_counts = created_counts; - return SG_OK; +sg_status sg_automaton_find_action(const sg_automaton *automaton, const char *action_key, + size_t *action) { + return automaton == NULL + ? SG_ERR_INVALID_ARGUMENT + : sg_find_key(automaton->action_keys, automaton->action_count, action_key, action); } -void sg_explain_free(size_t *steps, size_t *step_counts) { - free(steps); - free(step_counts); +sg_status sg_automaton_find_output(const sg_automaton *automaton, const char *output_key, + size_t *output) { + return automaton == NULL + ? SG_ERR_INVALID_ARGUMENT + : sg_find_key(automaton->output_keys, automaton->output_count, output_key, output); } diff --git a/src/sync_cli.c b/src/sync_cli.c index a8e4da4..38f0f70 100644 --- a/src/sync_cli.c +++ b/src/sync_cli.c @@ -4,134 +4,149 @@ #include #include -static int add_transition(sg_dfa_builder *builder, const char *source, const char *letter, - const char *target) { - if (sg_dfa_builder_add_transition(builder, source, letter, target) != SG_OK) { - return 1; +typedef struct { + const char *source; + const char *action; + const char *target; + const char *output; +} machine_cell; + +static sg_status build_warehouse(sg_automaton **automaton) { + static const char *const states[] = { + "west_bay:east", "east_bay:west", "corridor_w:east", "corridor_e:west", "dock:north", + }; + static const char *const actions[] = { + "to_corridor", + "to_wall", + "go_west", + "go_east", + }; + static const char *const outputs[] = { + "west_landmark", + "east_landmark", + "symmetric", + "dock", + }; + static const machine_cell cells[] = { + {"west_bay:east", "to_corridor", "corridor_w:east", "west_landmark"}, + {"west_bay:east", "to_wall", "west_bay:east", "symmetric"}, + {"west_bay:east", "go_west", "west_bay:east", "symmetric"}, + {"west_bay:east", "go_east", "west_bay:east", "symmetric"}, + {"east_bay:west", "to_corridor", "corridor_e:west", "east_landmark"}, + {"east_bay:west", "to_wall", "east_bay:west", "symmetric"}, + {"east_bay:west", "go_west", "east_bay:west", "symmetric"}, + {"east_bay:west", "go_east", "east_bay:west", "symmetric"}, + {"corridor_w:east", "to_corridor", "corridor_w:east", "symmetric"}, + {"corridor_w:east", "to_wall", "west_bay:east", "west_landmark"}, + {"corridor_w:east", "go_west", "dock:north", "dock"}, + {"corridor_w:east", "go_east", "corridor_w:east", "symmetric"}, + {"corridor_e:west", "to_corridor", "corridor_e:west", "symmetric"}, + {"corridor_e:west", "to_wall", "east_bay:west", "east_landmark"}, + {"corridor_e:west", "go_west", "dock:north", "dock"}, + {"corridor_e:west", "go_east", "corridor_e:west", "symmetric"}, + {"dock:north", "to_corridor", "dock:north", "dock"}, + {"dock:north", "to_wall", "dock:north", "dock"}, + {"dock:north", "go_west", "dock:north", "dock"}, + {"dock:north", "go_east", "dock:north", "dock"}, + }; + + sg_automaton_builder *builder = NULL; + sg_status status = sg_automaton_builder_init(&builder); + for (size_t index = 0U; status == SG_OK && index < sizeof(states) / sizeof(states[0]); ++index) { + status = sg_automaton_builder_add_state(builder, states[index]); } - return 0; -} - -static sg_status build_office(sg_dfa **dfa) { - sg_dfa_builder *builder = NULL; - sg_status status = sg_dfa_builder_init(&builder); - if (status != SG_OK) { - return status; + for (size_t index = 0U; status == SG_OK && index < sizeof(actions) / sizeof(actions[0]); + ++index) { + status = sg_automaton_builder_add_action(builder, actions[index]); } - - const char *states[] = {"A", "B", "C"}; - const char *letters[] = {"north", "east"}; - for (size_t i = 0U; i < sizeof(states) / sizeof(states[0]); ++i) { - status = sg_dfa_builder_add_state(builder, states[i]); - if (status != SG_OK) { - sg_dfa_builder_free(builder); - return status; - } + for (size_t index = 0U; status == SG_OK && index < sizeof(outputs) / sizeof(outputs[0]); + ++index) { + status = sg_automaton_builder_add_output(builder, outputs[index]); } - for (size_t i = 0U; i < sizeof(letters) / sizeof(letters[0]); ++i) { - status = sg_dfa_builder_add_letter(builder, letters[i]); - if (status != SG_OK) { - sg_dfa_builder_free(builder); - return status; + for (size_t index = 0U; status == SG_OK && index < sizeof(cells) / sizeof(cells[0]); ++index) { + status = sg_automaton_builder_add_transition(builder, cells[index].source, cells[index].action, + cells[index].target); + if (status == SG_OK) { + status = sg_automaton_builder_add_observation(builder, cells[index].source, + cells[index].action, cells[index].output); } } - - if (add_transition(builder, "A", "north", "B") != 0 || - add_transition(builder, "B", "north", "C") != 0 || - add_transition(builder, "C", "north", "C") != 0 || - add_transition(builder, "A", "east", "A") != 0 || - add_transition(builder, "B", "east", "B") != 0 || - add_transition(builder, "C", "east", "C") != 0) { - sg_dfa_builder_free(builder); - return SG_ERR_ALLOC; + if (status == SG_OK) { + status = sg_automaton_builder_build(builder, 1U, automaton); } - - status = sg_dfa_builder_build(builder, false, dfa); - sg_dfa_builder_free(builder); + sg_automaton_builder_free(builder); return status; } -static void print_word(const sg_dfa *dfa, const sg_word *word) { +static void print_word(const sg_automaton *automaton, const sg_word *word) { printf("["); - for (size_t i = 0U; i < word->length; ++i) { - printf("%s\"%s\"", (i == 0U) ? "" : ", ", sg_dfa_letter_key(dfa, word->letters[i])); + for (size_t index = 0U; index < word->length; ++index) { + printf("%s\"%s\"", index == 0U ? "" : ", ", + sg_automaton_action_key(automaton, word->actions[index])); } printf("]"); } -static int run_office_example(void) { - sg_dfa *dfa = NULL; - sg_status status = build_office(&dfa); +static int run_warehouse_example(void) { + sg_automaton *automaton = NULL; + sg_status status = build_warehouse(&automaton); if (status != SG_OK) { - fprintf(stderr, "build_office failed: %s\n", sg_status_name(status)); - return 1; + fprintf(stderr, "build failed: %s\n", sg_status_name(status)); + return EXIT_FAILURE; } - sg_pair_oracle *oracle = NULL; - status = sg_pair_oracle_build(dfa, &oracle); + status = sg_pair_oracle_build(automaton, &oracle); if (status != SG_OK) { - fprintf(stderr, "pair oracle failed: %s\n", sg_status_name(status)); - sg_dfa_free(dfa); - return 1; + fprintf(stderr, "prepare failed: %s\n", sg_status_name(status)); + sg_automaton_free(automaton); + return EXIT_FAILURE; } + size_t initial[2] = {0U}; + (void)sg_automaton_find_state(automaton, "west_bay:east", &initial[0]); + (void)sg_automaton_find_state(automaton, "east_bay:west", &initial[1]); - size_t initial[2] = {0U, 0U}; - size_t target[1] = {0U}; - (void)sg_dfa_find_state(dfa, "A", &initial[0]); - (void)sg_dfa_find_state(dfa, "B", &initial[1]); - (void)sg_dfa_find_state(dfa, "C", &target[0]); - - sg_word_result result = {0}; - status = - sg_word_for_set(dfa, oracle, initial, 2U, target, 1U, SG_MODE_REACH_AND_SYNC, 64U, &result); - if (status != SG_OK) { - fprintf(stderr, "word_for_set failed: %s\n", sg_status_name(status)); - sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); - return 1; - } - - printf("status: %s\n", sg_result_kind_name(result.kind)); - printf("word: "); - print_word(dfa, &result.word); - printf("\nlength: %zu\n", result.word.length); - printf("final_state: %s\n", sg_dfa_state_key(dfa, result.final_state)); - - size_t *steps = NULL; - size_t *counts = NULL; - size_t step_count = 0U; - status = sg_explain_word(dfa, initial, 2U, &result.word, &steps, &counts, &step_count); + sg_plan_result sync = {0}; + status = sg_plan_sync(automaton, oracle, initial, 2U, 64U, &sync); if (status != SG_OK) { - fprintf(stderr, "explain failed: %s\n", sg_status_name(status)); - sg_word_result_free(&result); + fprintf(stderr, "synchronization failed: %s\n", sg_status_name(status)); sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); - return 1; + sg_automaton_free(automaton); + return EXIT_FAILURE; } - - for (size_t step = 0U; step < step_count; ++step) { - printf("step %zu active:", step); - for (size_t i = 0U; i < counts[step]; ++i) { - const size_t state = steps[(step * sg_dfa_state_count(dfa)) + i]; - printf(" %s", sg_dfa_state_key(dfa, state)); - } - printf("\n"); + printf("generation: %llu\n", (unsigned long long)sync.generation); + printf("sync.outcome: %s\n", sg_plan_outcome_name(sync.outcome)); + printf("sync.method: %s\n", sg_plan_method_name(sync.method)); + printf("sync.word: "); + print_word(automaton, &sync.word); + printf("\nsync.final_state: %s\n", sg_automaton_state_key(automaton, sync.final_state)); + + sg_plan_result reveal = {0}; + status = sg_plan_disambiguate(automaton, oracle, initial, 2U, 1U, 64U, &reveal); + if (status == SG_OK) { + printf("reveal.outcome: %s\n", sg_plan_outcome_name(reveal.outcome)); + printf("reveal.method: %s\n", sg_plan_method_name(reveal.method)); + printf("reveal.word: "); + print_word(automaton, &reveal.word); + printf("\nreveal.branches: %zu\n", reveal.branch_count); + printf("reveal.worst_support: %zu\n", reveal.worst_support_size); + } else { + fprintf(stderr, "disambiguation failed: %s\n", sg_status_name(status)); } - sg_explain_free(steps, counts); - sg_word_result_free(&result); + sg_plan_result_free(&reveal); + sg_plan_result_free(&sync); sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); - return 0; + sg_automaton_free(automaton); + return status == SG_OK ? EXIT_SUCCESS : EXIT_FAILURE; } -static void print_usage(const char *argv0) { - fprintf(stderr, "usage: %s --example office\n", argv0); +static void print_usage(const char *program) { + fprintf(stderr, "usage: %s --example warehouse\n", program); } int main(int argc, char **argv) { - if (argc == 3 && strcmp(argv[1], "--example") == 0 && strcmp(argv[2], "office") == 0) { - return run_office_example(); + if (argc == 3 && strcmp(argv[1], "--example") == 0 && strcmp(argv[2], "warehouse") == 0) { + return run_warehouse_example(); } print_usage(argv[0]); return 2; diff --git a/src/sync_internal.h b/src/sync_internal.h new file mode 100644 index 0000000..1e3da09 --- /dev/null +++ b/src/sync_internal.h @@ -0,0 +1,43 @@ +#ifndef SYNC_KGRAPH_SYNC_INTERNAL_H +#define SYNC_KGRAPH_SYNC_INTERNAL_H + +#include "sync_kgraph/sync.h" + +#include +#include +#include + +struct sg_automaton { + uint64_t generation; + size_t state_count; + size_t action_count; + size_t output_count; + char **state_keys; + char **action_keys; + char **output_keys; + size_t *transitions; + size_t *observations; +}; + +struct sg_pair_oracle { + const sg_automaton *automaton; + size_t pair_count; + size_t *first; + size_t *second; + size_t *next; + bool *outputs_differ; + size_t *merge_distance; + size_t *merge_action; + size_t *merge_next; + size_t *resolution_distance; + size_t *resolution_action; + size_t *resolution_next; +}; + +bool sg_size_multiply(size_t first, size_t second, size_t *product); +char *sg_string_duplicate(const char *value); +sg_status sg_word_extend(sg_word *destination, const sg_word *source); +size_t sg_pair_index(size_t state_count, size_t first, size_t second); +uint64_t sg_monotonic_time_us(void); + +#endif diff --git a/tests/test_core.c b/tests/test_core.c index 6008966..bb76fe3 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -8,259 +8,360 @@ do { \ if (!(condition)) { \ fprintf(stderr, "check failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \ - exit(1); \ + exit(EXIT_FAILURE); \ } \ } while (0) -static sg_dfa *build_office(void) { - sg_dfa_builder *builder = NULL; - CHECK(sg_dfa_builder_init(&builder) == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "A") == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "B") == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "C") == SG_OK); - CHECK(sg_dfa_builder_add_letter(builder, "north") == SG_OK); - CHECK(sg_dfa_builder_add_letter(builder, "east") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "A", "north", "B") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "B", "north", "C") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "C", "north", "C") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "A", "east", "A") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "B", "east", "B") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "C", "east", "C") == SG_OK); - sg_dfa *dfa = NULL; - CHECK(sg_dfa_builder_build(builder, false, &dfa) == SG_OK); - sg_dfa_builder_free(builder); - return dfa; -} +typedef struct { + const char *source; + const char *action; + const char *target; + const char *output; +} machine_cell; typedef struct { size_t calls; - size_t nonempty; -} cache_visit_counts; + size_t initial_rows; + size_t action_rows; + size_t singleton_rows; +} explain_counts; + +static void add_keys(sg_automaton_builder *builder, const char *const *states, size_t state_count, + const char *const *actions, size_t action_count, const char *const *outputs, + size_t output_count) { + for (size_t index = 0U; index < state_count; ++index) { + CHECK(sg_automaton_builder_add_state(builder, states[index]) == SG_OK); + } + for (size_t index = 0U; index < action_count; ++index) { + CHECK(sg_automaton_builder_add_action(builder, actions[index]) == SG_OK); + } + for (size_t index = 0U; index < output_count; ++index) { + CHECK(sg_automaton_builder_add_output(builder, outputs[index]) == SG_OK); + } +} + +static void add_cells(sg_automaton_builder *builder, const machine_cell *cells, size_t cell_count) { + for (size_t index = 0U; index < cell_count; ++index) { + CHECK(sg_automaton_builder_add_transition(builder, cells[index].source, cells[index].action, + cells[index].target) == SG_OK); + CHECK(sg_automaton_builder_add_observation(builder, cells[index].source, cells[index].action, + cells[index].output) == SG_OK); + } +} + +static sg_automaton *build_warehouse(void) { + static const char *const states[] = { + "west_bay:east", "east_bay:west", "corridor_w:east", "corridor_e:west", "dock:north", + }; + static const char *const actions[] = { + "to_corridor", + "to_wall", + "go_west", + "go_east", + }; + static const char *const outputs[] = { + "west_landmark", + "east_landmark", + "symmetric", + "dock", + }; + static const machine_cell cells[] = { + {"west_bay:east", "to_corridor", "corridor_w:east", "west_landmark"}, + {"west_bay:east", "to_wall", "west_bay:east", "symmetric"}, + {"west_bay:east", "go_west", "west_bay:east", "symmetric"}, + {"west_bay:east", "go_east", "west_bay:east", "symmetric"}, + {"east_bay:west", "to_corridor", "corridor_e:west", "east_landmark"}, + {"east_bay:west", "to_wall", "east_bay:west", "symmetric"}, + {"east_bay:west", "go_west", "east_bay:west", "symmetric"}, + {"east_bay:west", "go_east", "east_bay:west", "symmetric"}, + {"corridor_w:east", "to_corridor", "corridor_w:east", "symmetric"}, + {"corridor_w:east", "to_wall", "west_bay:east", "west_landmark"}, + {"corridor_w:east", "go_west", "dock:north", "dock"}, + {"corridor_w:east", "go_east", "corridor_w:east", "symmetric"}, + {"corridor_e:west", "to_corridor", "corridor_e:west", "symmetric"}, + {"corridor_e:west", "to_wall", "east_bay:west", "east_landmark"}, + {"corridor_e:west", "go_west", "dock:north", "dock"}, + {"corridor_e:west", "go_east", "corridor_e:west", "symmetric"}, + {"dock:north", "to_corridor", "dock:north", "dock"}, + {"dock:north", "to_wall", "dock:north", "dock"}, + {"dock:north", "go_west", "dock:north", "dock"}, + {"dock:north", "go_east", "dock:north", "dock"}, + }; -static sg_status count_cache_visit(void *ctx, const size_t *states, size_t state_count, - const sg_word *word) { - cache_visit_counts *counts = ctx; + sg_automaton_builder *builder = NULL; + CHECK(sg_automaton_builder_init(&builder) == SG_OK); + add_keys(builder, states, sizeof(states) / sizeof(states[0]), actions, + sizeof(actions) / sizeof(actions[0]), outputs, sizeof(outputs) / sizeof(outputs[0])); + add_cells(builder, cells, sizeof(cells) / sizeof(cells[0])); + sg_automaton *automaton = NULL; + CHECK(sg_automaton_builder_build(builder, UINT64_C(7), &automaton) == SG_OK); + sg_automaton_builder_free(builder); + return automaton; +} + +static sg_automaton *build_two_step_observer(void) { + static const char *const states[] = {"A", "B", "C"}; + static const char *const actions[] = {"ask_a", "ask_b"}; + static const char *const outputs[] = {"yes", "no"}; + static const machine_cell cells[] = { + {"A", "ask_a", "A", "yes"}, {"A", "ask_b", "A", "no"}, {"B", "ask_a", "B", "no"}, + {"B", "ask_b", "B", "yes"}, {"C", "ask_a", "C", "no"}, {"C", "ask_b", "C", "no"}, + }; + + sg_automaton_builder *builder = NULL; + CHECK(sg_automaton_builder_init(&builder) == SG_OK); + add_keys(builder, states, sizeof(states) / sizeof(states[0]), actions, + sizeof(actions) / sizeof(actions[0]), outputs, sizeof(outputs) / sizeof(outputs[0])); + add_cells(builder, cells, sizeof(cells) / sizeof(cells[0])); + sg_automaton *automaton = NULL; + CHECK(sg_automaton_builder_build(builder, UINT64_C(11), &automaton) == SG_OK); + sg_automaton_builder_free(builder); + return automaton; +} + +static size_t state_id(const sg_automaton *automaton, const char *key) { + size_t state = SG_INDEX_NONE; + CHECK(sg_automaton_find_state(automaton, key, &state) == SG_OK); + return state; +} + +static size_t action_id(const sg_automaton *automaton, const char *key) { + size_t action = SG_INDEX_NONE; + CHECK(sg_automaton_find_action(automaton, key, &action) == SG_OK); + return action; +} + +static sg_status count_explanation(void *context, size_t step, size_t action, + const size_t *predicted_states, size_t predicted_count, + const size_t *output_trace, size_t trace_length, + const size_t *branch_states, size_t branch_count) { + explain_counts *counts = context; CHECK(counts != NULL); - CHECK(word != NULL); - CHECK(states != NULL || state_count == 0U); + CHECK(predicted_states != NULL); + CHECK(predicted_count != 0U); + CHECK(branch_states != NULL); + CHECK(branch_count != 0U); + CHECK(output_trace != NULL || trace_length == 0U); ++counts->calls; - if (state_count != 0U) { - ++counts->nonempty; + if (step == 0U) { + CHECK(action == SG_INDEX_NONE); + CHECK(trace_length == 0U); + ++counts->initial_rows; + } else { + CHECK(action != SG_INDEX_NONE); + CHECK(trace_length == step); + ++counts->action_rows; + } + if (branch_count == 1U) { + ++counts->singleton_rows; } return SG_OK; } -static void test_builder_validation(void) { - sg_dfa_builder *builder = NULL; - CHECK(sg_dfa_builder_init(&builder) == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "S") == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "T") == SG_OK); - CHECK(sg_dfa_builder_add_letter(builder, "a") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "S", "a", "T") == SG_OK); - - sg_dfa *dfa = NULL; - CHECK(sg_dfa_builder_build(builder, false, &dfa) == SG_ERR_INCOMPLETE); - CHECK(dfa == NULL); - CHECK(sg_dfa_builder_build(builder, true, &dfa) == SG_OK); - CHECK(sg_dfa_state_count(dfa) == 3U); - CHECK(strcmp(sg_dfa_state_key(dfa, 2U), "__sink") == 0); - sg_dfa_free(dfa); - sg_dfa_builder_free(builder); - - CHECK(sg_dfa_builder_init(&builder) == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "S") == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "T") == SG_OK); - CHECK(sg_dfa_builder_add_letter(builder, "a") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "S", "a", "S") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "S", "a", "T") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "T", "a", "T") == SG_OK); - CHECK(sg_dfa_builder_build(builder, false, &dfa) == SG_ERR_NONDETERMINISTIC); - sg_dfa_builder_free(builder); +static void test_names_and_builder_validation(void) { + CHECK(strcmp(sg_status_name(SG_ERR_STALE_GENERATION), "STALE_GENERATION") == 0); + CHECK(strcmp(sg_plan_outcome_name(SG_OUTCOME_RESOURCE_BOUND), "RESOURCE_BOUND") == 0); + CHECK(strcmp(sg_plan_method_name(SG_METHOD_PARTITION_BFS), "PARTITION_BFS") == 0); + CHECK(strcmp(sg_monitor_decision_name(SG_MONITOR_MODEL_VIOLATION), "MODEL_VIOLATION") == 0); + + sg_automaton_builder *builder = NULL; + CHECK(sg_automaton_builder_init(&builder) == SG_OK); + CHECK(sg_automaton_builder_add_state(builder, "S") == SG_OK); + CHECK(sg_automaton_builder_add_state(builder, "T") == SG_OK); + CHECK(sg_automaton_builder_add_state(builder, "S") == SG_ERR_DUPLICATE); + CHECK(sg_automaton_builder_add_action(builder, "a") == SG_OK); + CHECK(sg_automaton_builder_add_output(builder, "quiet") == SG_OK); + CHECK(sg_automaton_builder_add_transition(builder, "S", "a", "T") == SG_OK); + CHECK(sg_automaton_builder_add_observation(builder, "S", "a", "quiet") == SG_OK); + sg_automaton *automaton = NULL; + CHECK(sg_automaton_builder_build(builder, 1U, &automaton) == SG_ERR_INCOMPLETE); + CHECK(automaton == NULL); + CHECK(sg_automaton_builder_add_transition(builder, "T", "a", "T") == SG_OK); + CHECK(sg_automaton_builder_add_transition(builder, "T", "a", "S") == SG_OK); + CHECK(sg_automaton_builder_add_observation(builder, "T", "a", "quiet") == SG_OK); + CHECK(sg_automaton_builder_build(builder, 1U, &automaton) == SG_ERR_NONDETERMINISTIC); + sg_automaton_builder_free(builder); } -static void test_pair_oracle_and_greedy_word(void) { - sg_dfa *dfa = build_office(); +static void test_automaton_and_oracle(void) { + sg_automaton *automaton = build_warehouse(); + CHECK(sg_automaton_generation(automaton) == 7U); + CHECK(sg_automaton_state_count(automaton) == 5U); + CHECK(sg_automaton_action_count(automaton) == 4U); + CHECK(sg_automaton_output_count(automaton) == 4U); + CHECK(sg_automaton_transition_count(automaton) == 20U); + CHECK(sg_automaton_find_state(automaton, "missing", &(size_t){0U}) == SG_ERR_NOT_FOUND); + sg_pair_oracle *oracle = NULL; - CHECK(sg_pair_oracle_build(dfa, &oracle) == SG_OK); - CHECK(sg_pair_oracle_pair_count(oracle) == 6U); - CHECK(sg_pair_oracle_pair_edge_count(oracle) == 12U); - - size_t a = 0U; - size_t b = 0U; - size_t c = 0U; - size_t north = 0U; - CHECK(sg_dfa_find_state(dfa, "A", &a) == SG_OK); - CHECK(sg_dfa_find_state(dfa, "B", &b) == SG_OK); - CHECK(sg_dfa_find_state(dfa, "C", &c) == SG_OK); - CHECK(sg_dfa_find_letter(dfa, "north", &north) == SG_OK); - CHECK(sg_pair_oracle_has_witness(oracle, a, b)); - - size_t first = 0U; - size_t second = 0U; - CHECK(sg_pair_oracle_pair_states(oracle, 1U, &first, &second) == SG_OK); - CHECK(first == a); - CHECK(second == b); - size_t next_pair = 0U; - CHECK(sg_pair_oracle_pair_next(oracle, 1U, north, &next_pair) == SG_OK); - CHECK(next_pair == 4U); - bool has_witness = false; - size_t distance = 0U; - size_t letter = 0U; - CHECK(sg_pair_oracle_pair_witness(oracle, 1U, &has_witness, &distance, &letter, &next_pair) == - SG_OK); - CHECK(has_witness); - CHECK(distance == 2U); - CHECK(letter == north); - CHECK(next_pair == 4U); - - sg_word witness = {0}; - CHECK(sg_pair_oracle_witness_word(oracle, a, b, &witness) == SG_OK); - CHECK(witness.length == 2U); - CHECK(strcmp(sg_dfa_letter_key(dfa, witness.letters[0]), "north") == 0); - CHECK(strcmp(sg_dfa_letter_key(dfa, witness.letters[1]), "north") == 0); - sg_word_free(&witness); - - size_t initial[2] = {a, b}; - size_t target[1] = {c}; - sg_word_result result = {0}; - CHECK(sg_word_for_set(dfa, oracle, initial, 2U, target, 1U, SG_MODE_REACH_AND_SYNC, 16U, - &result) == SG_OK); - CHECK(result.kind == SG_RESULT_PAIR_GREEDY_TARGETED); - CHECK(result.word.length == 2U); - CHECK(result.final_state == c); - - size_t out[3] = {0U, 0U, 0U}; - size_t out_count = 0U; - CHECK(sg_apply_word_to_set(dfa, initial, 2U, &result.word, out, &out_count) == SG_OK); - CHECK(out_count == 1U); - CHECK(out[0] == c); - - size_t *steps = NULL; - size_t *counts = NULL; - size_t step_count = 0U; - CHECK(sg_explain_word(dfa, initial, 2U, &result.word, &steps, &counts, &step_count) == SG_OK); - CHECK(step_count == 3U); - CHECK(counts[0] == 2U); - CHECK(counts[1] == 2U); - CHECK(counts[2] == 1U); - CHECK(steps[(2U * sg_dfa_state_count(dfa))] == c); - sg_explain_free(steps, counts); - - sg_word_result_free(&result); - sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); -} + CHECK(sg_pair_oracle_build(automaton, &oracle) == SG_OK); + CHECK(sg_pair_oracle_pair_count(oracle) == 15U); + CHECK(sg_pair_oracle_pair_edge_count(oracle) == 60U); + CHECK(sg_pair_oracle_mergeable_pair_count(oracle) == 15U); + CHECK(sg_pair_oracle_resolvable_pair_count(oracle) == 15U); + + const size_t west = state_id(automaton, "west_bay:east"); + const size_t east = state_id(automaton, "east_bay:west"); + const size_t to_corridor = action_id(automaton, "to_corridor"); + size_t west_east_pair = SG_INDEX_NONE; + for (size_t pair = 0U; pair < sg_pair_oracle_pair_count(oracle); ++pair) { + size_t first = 0U; + size_t second = 0U; + CHECK(sg_pair_oracle_pair_states(oracle, pair, &first, &second) == SG_OK); + if (first == west && second == east) { + west_east_pair = pair; + } + } + CHECK(west_east_pair != SG_INDEX_NONE); + bool outputs_differ = false; + size_t next_pair = SG_INDEX_NONE; + CHECK(sg_pair_oracle_pair_step(oracle, west_east_pair, to_corridor, &next_pair, + &outputs_differ) == SG_OK); + CHECK(outputs_differ); + CHECK(next_pair != SG_INDEX_NONE); -static sg_dfa *build_reach_only_machine(void) { - sg_dfa_builder *builder = NULL; - CHECK(sg_dfa_builder_init(&builder) == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "A") == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "B") == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "C") == SG_OK); - CHECK(sg_dfa_builder_add_state(builder, "D") == SG_OK); - CHECK(sg_dfa_builder_add_letter(builder, "go") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "A", "go", "C") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "B", "go", "D") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "C", "go", "C") == SG_OK); - CHECK(sg_dfa_builder_add_transition(builder, "D", "go", "D") == SG_OK); - sg_dfa *dfa = NULL; - CHECK(sg_dfa_builder_build(builder, false, &dfa) == SG_OK); - sg_dfa_builder_free(builder); - return dfa; + sg_word merge = {0}; + CHECK(sg_pair_oracle_merge_word(oracle, west, east, &merge) == SG_OK); + CHECK(merge.length == 2U); + CHECK(strcmp(sg_automaton_action_key(automaton, merge.actions[0]), "to_corridor") == 0); + CHECK(strcmp(sg_automaton_action_key(automaton, merge.actions[1]), "go_west") == 0); + sg_word_free(&merge); + + sg_word resolution = {0}; + CHECK(sg_pair_oracle_resolution_word(oracle, west, east, &resolution) == SG_OK); + CHECK(resolution.length == 1U); + CHECK(resolution.actions[0] == to_corridor); + sg_word_free(&resolution); + + const size_t pair_count = sg_pair_oracle_pair_count(oracle); + sg_pair_record *records = calloc(pair_count, sizeof(*records)); + CHECK(records != NULL); + for (size_t pair = 0U; pair < pair_count; ++pair) { + CHECK(sg_pair_oracle_record(oracle, pair, &records[pair]) == SG_OK); + } + sg_pair_oracle *restored = NULL; + CHECK(sg_pair_oracle_restore(automaton, records, pair_count, &restored) == SG_OK); + CHECK(sg_pair_oracle_mergeable_pair_count(restored) == pair_count); + sg_pair_oracle_free(restored); + records[west_east_pair].resolution_action = SG_INDEX_NONE; + CHECK(sg_pair_oracle_restore(automaton, records, pair_count, &restored) == SG_ERR_INVALID_MODEL); + free(records); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); } -static void test_exact_reach_mode(void) { - sg_dfa *dfa = build_reach_only_machine(); +static void test_planners_explanation_and_monitor(void) { + sg_automaton *automaton = build_warehouse(); sg_pair_oracle *oracle = NULL; - CHECK(sg_pair_oracle_build(dfa, &oracle) == SG_OK); - - size_t a = 0U; - size_t b = 0U; - size_t c = 0U; - size_t d = 0U; - CHECK(sg_dfa_find_state(dfa, "A", &a) == SG_OK); - CHECK(sg_dfa_find_state(dfa, "B", &b) == SG_OK); - CHECK(sg_dfa_find_state(dfa, "C", &c) == SG_OK); - CHECK(sg_dfa_find_state(dfa, "D", &d) == SG_OK); - - size_t initial[2] = {a, b}; - size_t targets[2] = {c, d}; - sg_word_result result = {0}; - CHECK(sg_word_for_set(dfa, oracle, initial, 2U, targets, 2U, SG_MODE_REACH, 8U, &result) == - SG_OK); - CHECK(result.kind == SG_RESULT_EXACT_EXPANDED); - CHECK(result.word.length == 1U); - CHECK(strcmp(sg_dfa_letter_key(dfa, result.word.letters[0]), "go") == 0); - sg_word_result_free(&result); - - size_t expanded = 0U; - size_t cache_size = 0U; - CHECK(sg_expand_cache(dfa, targets, 2U, SG_MODE_REACH, 8U, &expanded, &cache_size) == SG_OK); - CHECK(expanded >= 1U); - CHECK(cache_size >= expanded); - - cache_visit_counts visits = {0U, 0U}; - expanded = 0U; - cache_size = 0U; - CHECK(sg_expand_cache_visit(dfa, targets, 2U, SG_MODE_REACH, 8U, count_cache_visit, &visits, - &expanded, &cache_size) == SG_OK); - CHECK(visits.calls == expanded); - CHECK(visits.nonempty == visits.calls); - CHECK(cache_size >= expanded); - - expanded = 0U; - cache_size = 0U; - CHECK(sg_expand_cache(dfa, targets, 2U, SG_MODE_REACH_AND_SYNC, 8U, &expanded, &cache_size) == - SG_OK); - CHECK(expanded >= 1U); - CHECK(cache_size >= expanded); - - expanded = 0U; - cache_size = 0U; - CHECK(sg_expand_cache(dfa, NULL, 0U, SG_MODE_SYNC, 8U, &expanded, &cache_size) == SG_OK); - CHECK(expanded >= 1U); - CHECK(cache_size >= expanded); - - expanded = 8U; - cache_size = 8U; - CHECK(sg_expand_cache(dfa, targets, 2U, SG_MODE_REACH, 0U, &expanded, &cache_size) == - SG_ERR_RESOURCE_BOUND); - CHECK(expanded == 0U); - CHECK(cache_size == 0U); - - CHECK(sg_word_for_set(dfa, oracle, initial, 2U, targets, 2U, SG_MODE_REACH_AND_SYNC, 1U, - &result) == SG_ERR_RESOURCE_BOUND); - CHECK(result.kind == SG_RESULT_RESOURCE_BOUND); - sg_word_result_free(&result); + CHECK(sg_pair_oracle_build(automaton, &oracle) == SG_OK); + const size_t initial[] = { + state_id(automaton, "west_bay:east"), + state_id(automaton, "east_bay:west"), + }; + + sg_plan_result sync = {0}; + CHECK(sg_plan_sync(automaton, oracle, initial, 2U, 16U, &sync) == SG_OK); + CHECK(sync.outcome == SG_OUTCOME_PLAN); + CHECK(sync.method == SG_METHOD_PAIR_MERGE); + CHECK(sync.word.length == 2U); + CHECK(sync.final_state == state_id(automaton, "dock:north")); + CHECK(sync.final_support_size == 1U); + CHECK(sync.generation == 7U); + size_t final_states[sizeof(initial) / sizeof(initial[0])] = {0U}; + size_t final_count = 0U; + CHECK(sg_apply_word(automaton, initial, 2U, &sync.word, final_states, &final_count) == SG_OK); + CHECK(final_count == 1U); + CHECK(final_states[0] == sync.final_state); + + explain_counts explanation = {0}; + CHECK(sg_explain_plan(automaton, sync.generation, initial, 2U, &sync.word, count_explanation, + &explanation) == SG_OK); + CHECK(explanation.calls == 5U); + CHECK(explanation.initial_rows == 1U); + CHECK(explanation.action_rows == 4U); + CHECK(explanation.singleton_rows == 4U); + CHECK(sg_explain_plan(automaton, sync.generation + 1U, initial, 2U, &sync.word, count_explanation, + &explanation) == SG_ERR_STALE_GENERATION); + + const size_t corridor[] = { + state_id(automaton, "corridor_w:east"), + state_id(automaton, "corridor_e:west"), + }; + sg_monitor_result monitor = {0}; + CHECK(sg_validate_update(automaton, sync.generation, initial, 2U, &sync.word, 1U, corridor, 2U, + true, &monitor) == SG_OK); + CHECK(monitor.decision == SG_MONITOR_CONTINUE); + CHECK(monitor.expected_count == 2U); + sg_monitor_result_free(&monitor); + CHECK(sg_validate_update(automaton, sync.generation, initial, 2U, &sync.word, 1U, corridor, 1U, + true, &monitor) == SG_OK); + CHECK(monitor.decision == SG_MONITOR_REPLAN); + sg_monitor_result_free(&monitor); + const size_t dock[] = {state_id(automaton, "dock:north")}; + CHECK(sg_validate_update(automaton, sync.generation, initial, 2U, &sync.word, 1U, dock, 1U, true, + &monitor) == SG_OK); + CHECK(monitor.decision == SG_MONITOR_MODEL_VIOLATION); + CHECK(monitor.unexpected_count == 1U); + sg_monitor_result_free(&monitor); + CHECK(sg_validate_update(automaton, sync.generation, initial, 2U, &sync.word, 1U, NULL, 0U, false, + &monitor) == SG_OK); + CHECK(monitor.decision == SG_MONITOR_WAIT); + sg_monitor_result_free(&monitor); + CHECK(sg_validate_update(automaton, sync.generation + 1U, initial, 2U, &sync.word, 1U, corridor, + 2U, true, &monitor) == SG_OK); + CHECK(monitor.decision == SG_MONITOR_STALE_GENERATION); + sg_monitor_result_free(&monitor); + + sg_plan_result disambiguation = {0}; + CHECK(sg_plan_disambiguate(automaton, oracle, initial, 2U, 1U, 16U, &disambiguation) == SG_OK); + CHECK(disambiguation.outcome == SG_OUTCOME_PLAN); + CHECK(disambiguation.method == SG_METHOD_PAIR_RESOLUTION); + CHECK(disambiguation.word.length == 1U); + CHECK(disambiguation.word.actions[0] == action_id(automaton, "to_corridor")); + CHECK(disambiguation.branch_count == 2U); + CHECK(disambiguation.worst_support_size == 1U); + CHECK(disambiguation.homing); + + sg_plan_result_free(&disambiguation); + sg_plan_result_free(&sync); sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); + sg_automaton_free(automaton); } -static void test_mode_parse(void) { - sg_mode mode = SG_MODE_SYNC; - CHECK(strcmp(sg_status_name(SG_ERR_ALLOC), "ALLOC") == 0); - CHECK(strcmp(sg_status_name(SG_ERR_DUPLICATE), "DUPLICATE") == 0); - CHECK(strcmp(sg_status_name(SG_ERR_INCOMPLETE), "INCOMPLETE") == 0); - CHECK(strcmp(sg_status_name(SG_ERR_NONDETERMINISTIC), "NONDETERMINISTIC") == 0); - CHECK(strcmp(sg_status_name(SG_ERR_UNSYNCHRONIZABLE), "UNSYNCHRONIZABLE") == 0); - CHECK(strcmp(sg_result_kind_name(SG_RESULT_TRIVIAL), "TRIVIAL") == 0); - CHECK(strcmp(sg_result_kind_name(SG_RESULT_PAIR_GREEDY), "PAIR_GREEDY") == 0); - CHECK(strcmp(sg_result_kind_name(SG_RESULT_FAILURE), "FAILURE") == 0); - CHECK(strcmp(sg_mode_name(SG_MODE_REACH_AND_SYNC), "REACH_AND_SYNC") == 0); - CHECK(sg_mode_parse("SYNC", &mode) == SG_OK); - CHECK(mode == SG_MODE_SYNC); - CHECK(sg_mode_parse("REACH", &mode) == SG_OK); - CHECK(mode == SG_MODE_REACH); - CHECK(sg_mode_parse("REACH_AND_SYNC", &mode) == SG_OK); - CHECK(mode == SG_MODE_REACH_AND_SYNC); - CHECK(sg_mode_parse("bad", &mode) == SG_ERR_INVALID_ARGUMENT); +static void test_exact_partition_search(void) { + sg_automaton *automaton = build_two_step_observer(); + sg_pair_oracle *oracle = NULL; + CHECK(sg_pair_oracle_build(automaton, &oracle) == SG_OK); + const size_t initial[] = { + state_id(automaton, "A"), + state_id(automaton, "B"), + state_id(automaton, "C"), + }; + + sg_plan_result result = {0}; + CHECK(sg_plan_disambiguate(automaton, oracle, initial, 3U, 1U, 1U, &result) == SG_OK); + CHECK(result.outcome == SG_OUTCOME_RESOURCE_BOUND); + sg_plan_result_free(&result); + + CHECK(sg_plan_disambiguate(automaton, oracle, initial, 3U, 1U, 16U, &result) == SG_OK); + CHECK(result.outcome == SG_OUTCOME_PLAN); + CHECK(result.method == SG_METHOD_PARTITION_BFS); + CHECK(result.word.length == 2U); + CHECK(result.worst_support_size == 1U); + CHECK(result.branch_count == 3U); + sg_plan_result_free(&result); + + CHECK(sg_plan_sync(automaton, oracle, initial, 3U, 16U, &result) == SG_OK); + CHECK(result.outcome == SG_OUTCOME_NO_PLAN); + sg_plan_result_free(&result); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); } int main(void) { - test_mode_parse(); - test_builder_validation(); - test_pair_oracle_and_greedy_word(); - test_exact_reach_mode(); - return 0; + test_names_and_builder_validation(); + test_automaton_and_oracle(); + test_planners_explanation_and_monitor(); + test_exact_partition_search(); + return EXIT_SUCCESS; } From 2eb33f1991622c8a4b118cf813a7ffe478e366af Mon Sep 17 00:00:00 2001 From: gaperez64 Date: Mon, 17 Aug 2026 21:36:16 +0200 Subject: [PATCH 5/7] Add synchronize-or-reveal Memgraph interface --- cypher/install_schema.cypher | 31 +- cypher/triggers_mark_dirty.cypher | 22 +- cypher/uninstall.cypher | 9 +- examples/office/00_reset_and_load.cypher | 26 - examples/office/01_validate.cypher | 11 - examples/office/02_build_oracle.cypher | 10 - examples/office/03_word_to_target.cypher | 16 - examples/office/04_explain.cypher | 9 - examples/office/README.md | 11 - examples/warehouse/00_reset_and_load.cypher | 90 + examples/warehouse/01_prepare.cypher | 8 + examples/warehouse/02_plan_sync.cypher | 13 + .../warehouse/03_plan_disambiguate.cypher | 14 + examples/warehouse/04_explain.cypher | 13 + examples/warehouse/05_monitor.cypher | 12 + examples/warehouse/README.md | 10 + meson.build | 2 +- src/memgraph/sync_module.c | 2144 +++++++++-------- views/sync_automata.gss | 26 +- 19 files changed, 1398 insertions(+), 1079 deletions(-) delete mode 100644 examples/office/00_reset_and_load.cypher delete mode 100644 examples/office/01_validate.cypher delete mode 100644 examples/office/02_build_oracle.cypher delete mode 100644 examples/office/03_word_to_target.cypher delete mode 100644 examples/office/04_explain.cypher delete mode 100644 examples/office/README.md create mode 100644 examples/warehouse/00_reset_and_load.cypher create mode 100644 examples/warehouse/01_prepare.cypher create mode 100644 examples/warehouse/02_plan_sync.cypher create mode 100644 examples/warehouse/03_plan_disambiguate.cypher create mode 100644 examples/warehouse/04_explain.cypher create mode 100644 examples/warehouse/05_monitor.cypher create mode 100644 examples/warehouse/README.md diff --git a/cypher/install_schema.cypher b/cypher/install_schema.cypher index a00e8b0..18063f9 100644 --- a/cypher/install_schema.cypher +++ b/cypher/install_schema.cypher @@ -1,27 +1,22 @@ -// Sync-KGraph auxiliary indexes and constraints. -// Run this once in an existing Memgraph database before loading a model view. +// Optional indexes for the manually materialized Sync-KGraph view. CREATE INDEX ON :SyncModel(model); CREATE INDEX ON :SyncState(model); CREATE INDEX ON :SyncState(state_key); -CREATE INDEX ON :SyncLetter(model); +CREATE INDEX ON :SyncAction(model); +CREATE INDEX ON :SyncAction(action_key); +CREATE INDEX ON :SyncOutput(model); +CREATE INDEX ON :SyncOutput(output_key); CREATE INDEX ON :SyncPair(model); CREATE INDEX ON :SyncPair(pair_id); -CREATE INDEX ON :SyncSubset(model); -CREATE INDEX ON :SyncSubset(subset_key); -// The application owns the source schema. Sync-KGraph only requires these -// materialized view objects: -// +// Application-owned view contract: // (:SyncModel {model, generation, dirty}) -// (:SyncState {model, state_key, state_id, base_id?}) -// (:SyncLetter {model, letter, letter_id}) -// (:SyncState)-[:SYNC_TRANS {model, letter}]->(:SyncState) +// (:SyncState {model, state_key, state_id, semantic_ref?, orientation?}) +// (:SyncAction {model, action_key, action_id}) +// (:SyncOutput {model, output_key, output_id}) +// (:SyncState)-[:SYNC_TRANS {model, action_key}]->(:SyncState) +// (:SyncState)-[:SYNC_OBS {model, action_key}]->(:SyncOutput) // -// The native module materializes: -// (:SyncPair {model, pair_id, first_key, second_key, distance, has_witness, -// witness, next_pair, generation}) -// (:SyncPair)-[:PAIR_NEXT {model, letter, letter_id}]->(:SyncPair) -// (:SyncPair)-[:PAIR_PRE {model, letter, letter_id}]->(:SyncPair) -// (:SyncSubset {model, mode, target_key, subset_key, word, size, -// word_length, generation}) +// sync.prepare_model materializes generation-scoped SyncPair records and, +// when requested, PAIR_NEXT and PAIR_PRE relationships. diff --git a/cypher/triggers_mark_dirty.cypher b/cypher/triggers_mark_dirty.cypher index 903e673..cd1099c 100644 --- a/cypher/triggers_mark_dirty.cypher +++ b/cypher/triggers_mark_dirty.cypher @@ -1,9 +1,13 @@ -// Coarse invalidation trigger. Adapt the MATCH predicate to the labels and -// relationship types that feed a deployment's automata view. - -CREATE TRIGGER sync_kgraph_mark_dirty -AFTER COMMIT -EXECUTE - MATCH (m:SyncModel) - SET m.dirty = true, - m.generation = coalesce(m.generation, 0) + 1; +// Trigger template only. A schema-agnostic trigger cannot determine which +// application updates feed a model without dirtying unrelated models or the +// writes performed by sync.prepare_model itself. +// +// Adapt the event predicate to the source labels and relationships used by +// your mapping, then call the generation update below for each affected model: +// +// MATCH (m:SyncModel {model: affected_model}) +// SET m.dirty = true, +// m.generation = coalesce(m.generation, 0) + 1; +// +// For explicit invalidation, use: +// CALL sync.mark_dirty(affected_model); diff --git a/cypher/uninstall.cypher b/cypher/uninstall.cypher index 7061c37..de236de 100644 --- a/cypher/uninstall.cypher +++ b/cypher/uninstall.cypher @@ -1,8 +1,9 @@ -// Remove Sync-KGraph auxiliary objects for every model. This intentionally -// leaves the application graph untouched. +// Remove only Sync-KGraph view and auxiliary objects. Application graph nodes +// outside these labels are intentionally left untouched. MATCH (n) -WHERE n:SyncModel OR n:SyncState OR n:SyncLetter OR n:SyncPair OR n:SyncSubset +WHERE n:SyncModel OR n:SyncState OR n:SyncAction OR n:SyncOutput OR n:SyncPair DETACH DELETE n; -DROP TRIGGER sync_kgraph_mark_dirty; +// Drop an adapted dirty-marking trigger separately if one was installed from +// cypher/triggers_mark_dirty.cypher. diff --git a/examples/office/00_reset_and_load.cypher b/examples/office/00_reset_and_load.cypher deleted file mode 100644 index a8f040a..0000000 --- a/examples/office/00_reset_and_load.cypher +++ /dev/null @@ -1,26 +0,0 @@ -// Fully materialized office automaton example. -// This reset only touches Sync-KGraph view objects for this example model. - -MATCH (n) -WHERE (n:SyncModel OR n:SyncState OR n:SyncLetter OR n:SyncPair OR n:SyncSubset) - AND n.model = "office" -DETACH DELETE n; - -CREATE (:SyncModel {model: "office", generation: 0, dirty: false}); - -CREATE (:SyncState {model: "office", state_key: "A", state_id: 0}); -CREATE (:SyncState {model: "office", state_key: "B", state_id: 1}); -CREATE (:SyncState {model: "office", state_key: "C", state_id: 2}); - -CREATE (:SyncLetter {model: "office", letter: "north", letter_id: 0}); -CREATE (:SyncLetter {model: "office", letter: "east", letter_id: 1}); - -MATCH (a:SyncState {model: "office", state_key: "A"}) -MATCH (b:SyncState {model: "office", state_key: "B"}) -MATCH (c:SyncState {model: "office", state_key: "C"}) -CREATE (a)-[:SYNC_TRANS {model: "office", letter: "north"}]->(b) -CREATE (b)-[:SYNC_TRANS {model: "office", letter: "north"}]->(c) -CREATE (c)-[:SYNC_TRANS {model: "office", letter: "north"}]->(c) -CREATE (a)-[:SYNC_TRANS {model: "office", letter: "east"}]->(a) -CREATE (b)-[:SYNC_TRANS {model: "office", letter: "east"}]->(b) -CREATE (c)-[:SYNC_TRANS {model: "office", letter: "east"}]->(c); diff --git a/examples/office/01_validate.cypher b/examples/office/01_validate.cypher deleted file mode 100644 index 9e900fc..0000000 --- a/examples/office/01_validate.cypher +++ /dev/null @@ -1,11 +0,0 @@ -CALL sync.validate_model("office") -YIELD ok, code, message, states, letters, transitions -RETURN ok, code, message, states, letters, transitions; - -// Expected: -// ok: true -// code: "OK" -// message: "model is complete and deterministic" -// states: 3 -// letters: 2 -// transitions: 6 diff --git a/examples/office/02_build_oracle.cypher b/examples/office/02_build_oracle.cypher deleted file mode 100644 index 626c5f0..0000000 --- a/examples/office/02_build_oracle.cypher +++ /dev/null @@ -1,10 +0,0 @@ -CALL sync.build_pair_oracle("office") -YIELD status, pairs, pair_edges, mergeable_pairs, generation -RETURN status, pairs, pair_edges, mergeable_pairs, generation; - -// Expected: -// status: "OK" -// pairs: 6 -// pair_edges: 12 -// mergeable_pairs: 6 -// generation: 0 diff --git a/examples/office/03_word_to_target.cypher b/examples/office/03_word_to_target.cypher deleted file mode 100644 index 11b3dd0..0000000 --- a/examples/office/03_word_to_target.cypher +++ /dev/null @@ -1,16 +0,0 @@ -CALL sync.word_to_target( - "office", - ["A", "B"], - ["C"], - "REACH_AND_SYNC", - 64 -) -YIELD status, word, length, final_state_key, generation -RETURN status, word, length, final_state_key, generation; - -// Expected: -// status: "PAIR_GREEDY_TARGETED" -// word: ["north", "north"] -// length: 2 -// final_state_key: "C" -// generation: 0 diff --git a/examples/office/04_explain.cypher b/examples/office/04_explain.cypher deleted file mode 100644 index 5a26212..0000000 --- a/examples/office/04_explain.cypher +++ /dev/null @@ -1,9 +0,0 @@ -CALL sync.explain("office", ["A", "B"], ["north", "north"]) -YIELD step, letter, active_state_keys -RETURN step, letter, active_state_keys -ORDER BY step; - -// Expected: -// step: 0, letter: "", active_state_keys: ["A", "B"] -// step: 1, letter: "north", active_state_keys: ["B", "C"] -// step: 2, letter: "north", active_state_keys: ["C"] diff --git a/examples/office/README.md b/examples/office/README.md deleted file mode 100644 index 9d9f413..0000000 --- a/examples/office/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Office Example - -Run the files in numeric order after installing `sync.so` into Memgraph's query -module directory and loading it with: - -```cypher -CALL mg.load("sync"); -``` - -The example materializes a three-state automaton and asks for a word that maps -the hypothesis set `["A", "B"]` into the target `["C"]`. diff --git a/examples/warehouse/00_reset_and_load.cypher b/examples/warehouse/00_reset_and_load.cypher new file mode 100644 index 0000000..a7d1e32 --- /dev/null +++ b/examples/warehouse/00_reset_and_load.cypher @@ -0,0 +1,90 @@ +// This reset is scoped to the worked-example model. +MATCH (n) +WHERE n.model = "warehouse" + AND (n:SyncModel OR n:SyncState OR n:SyncAction OR n:SyncOutput OR n:SyncPair) +DETACH DELETE n; + +CREATE (:SyncModel {model: "warehouse", generation: 1, dirty: true}); + +UNWIND [ + {key: "west_bay:east", id: 0, zone: "west_bay", orientation: "east"}, + {key: "east_bay:west", id: 1, zone: "east_bay", orientation: "west"}, + {key: "corridor_w:east", id: 2, zone: "corridor_w", orientation: "east"}, + {key: "corridor_e:west", id: 3, zone: "corridor_e", orientation: "west"}, + {key: "dock:north", id: 4, zone: "dock", orientation: "north"} +] AS row +CREATE (:SyncState { + model: "warehouse", + state_key: row.key, + state_id: row.id, + semantic_ref: row.zone, + orientation: row.orientation +}); + +UNWIND [ + {key: "to_corridor", id: 0}, + {key: "to_wall", id: 1}, + {key: "go_west", id: 2}, + {key: "go_east", id: 3} +] AS row +CREATE (:SyncAction {model: "warehouse", action_key: row.key, action_id: row.id}); + +UNWIND [ + {key: "west_landmark", id: 0}, + {key: "east_landmark", id: 1}, + {key: "symmetric", id: 2}, + {key: "dock", id: 3} +] AS row +CREATE (:SyncOutput {model: "warehouse", output_key: row.key, output_id: row.id}); + +UNWIND [ + {src: "west_bay:east", action: "to_corridor", dst: "corridor_w:east"}, + {src: "west_bay:east", action: "to_wall", dst: "west_bay:east"}, + {src: "west_bay:east", action: "go_west", dst: "west_bay:east"}, + {src: "west_bay:east", action: "go_east", dst: "west_bay:east"}, + {src: "east_bay:west", action: "to_corridor", dst: "corridor_e:west"}, + {src: "east_bay:west", action: "to_wall", dst: "east_bay:west"}, + {src: "east_bay:west", action: "go_west", dst: "east_bay:west"}, + {src: "east_bay:west", action: "go_east", dst: "east_bay:west"}, + {src: "corridor_w:east", action: "to_corridor", dst: "corridor_w:east"}, + {src: "corridor_w:east", action: "to_wall", dst: "west_bay:east"}, + {src: "corridor_w:east", action: "go_west", dst: "dock:north"}, + {src: "corridor_w:east", action: "go_east", dst: "corridor_w:east"}, + {src: "corridor_e:west", action: "to_corridor", dst: "corridor_e:west"}, + {src: "corridor_e:west", action: "to_wall", dst: "east_bay:west"}, + {src: "corridor_e:west", action: "go_west", dst: "dock:north"}, + {src: "corridor_e:west", action: "go_east", dst: "corridor_e:west"}, + {src: "dock:north", action: "to_corridor", dst: "dock:north"}, + {src: "dock:north", action: "to_wall", dst: "dock:north"}, + {src: "dock:north", action: "go_west", dst: "dock:north"}, + {src: "dock:north", action: "go_east", dst: "dock:north"} +] AS row +MATCH (src:SyncState {model: "warehouse", state_key: row.src}) +MATCH (dst:SyncState {model: "warehouse", state_key: row.dst}) +CREATE (src)-[:SYNC_TRANS {model: "warehouse", action_key: row.action}]->(dst); + +UNWIND [ + {src: "west_bay:east", action: "to_corridor", output: "west_landmark"}, + {src: "west_bay:east", action: "to_wall", output: "symmetric"}, + {src: "west_bay:east", action: "go_west", output: "symmetric"}, + {src: "west_bay:east", action: "go_east", output: "symmetric"}, + {src: "east_bay:west", action: "to_corridor", output: "east_landmark"}, + {src: "east_bay:west", action: "to_wall", output: "symmetric"}, + {src: "east_bay:west", action: "go_west", output: "symmetric"}, + {src: "east_bay:west", action: "go_east", output: "symmetric"}, + {src: "corridor_w:east", action: "to_corridor", output: "symmetric"}, + {src: "corridor_w:east", action: "to_wall", output: "west_landmark"}, + {src: "corridor_w:east", action: "go_west", output: "dock"}, + {src: "corridor_w:east", action: "go_east", output: "symmetric"}, + {src: "corridor_e:west", action: "to_corridor", output: "symmetric"}, + {src: "corridor_e:west", action: "to_wall", output: "east_landmark"}, + {src: "corridor_e:west", action: "go_west", output: "dock"}, + {src: "corridor_e:west", action: "go_east", output: "symmetric"}, + {src: "dock:north", action: "to_corridor", output: "dock"}, + {src: "dock:north", action: "to_wall", output: "dock"}, + {src: "dock:north", action: "go_west", output: "dock"}, + {src: "dock:north", action: "go_east", output: "dock"} +] AS row +MATCH (src:SyncState {model: "warehouse", state_key: row.src}) +MATCH (output:SyncOutput {model: "warehouse", output_key: row.output}) +CREATE (src)-[:SYNC_OBS {model: "warehouse", action_key: row.action}]->(output); diff --git a/examples/warehouse/01_prepare.cypher b/examples/warehouse/01_prepare.cypher new file mode 100644 index 0000000..55ef660 --- /dev/null +++ b/examples/warehouse/01_prepare.cypher @@ -0,0 +1,8 @@ +CALL sync.prepare_model("warehouse", true) +YIELD status, generation, states, actions, outputs, transitions, pairs, + pair_edges, mergeable_pairs, resolvable_pairs, materialized_pair_edges +RETURN status, generation, states, actions, outputs, transitions, pairs, + pair_edges, mergeable_pairs, resolvable_pairs, materialized_pair_edges; + +// Expected row: +// "OK", 1, 5, 4, 4, 20, 15, 60, 15, 15, true diff --git a/examples/warehouse/02_plan_sync.cypher b/examples/warehouse/02_plan_sync.cypher new file mode 100644 index 0000000..d94512a --- /dev/null +++ b/examples/warehouse/02_plan_sync.cypher @@ -0,0 +1,13 @@ +CALL sync.plan_sync( + "warehouse", + ["west_bay:east", "east_bay:west"], + 64 +) +YIELD status, outcome, method, word, length, final_state_key, + final_support_size, generation +RETURN status, outcome, method, word, length, final_state_key, + final_support_size, generation; + +// Expected row: +// "OK", "PLAN", "PAIR_MERGE", ["to_corridor", "go_west"], 2, +// "dock:north", 1, 1 diff --git a/examples/warehouse/03_plan_disambiguate.cypher b/examples/warehouse/03_plan_disambiguate.cypher new file mode 100644 index 0000000..0b240d2 --- /dev/null +++ b/examples/warehouse/03_plan_disambiguate.cypher @@ -0,0 +1,14 @@ +CALL sync.plan_disambiguate( + "warehouse", + ["west_bay:east", "east_bay:west"], + 1, + 64 +) +YIELD status, outcome, method, word, length, best_support_size, + worst_support_size, branch_count, homing, generation +RETURN status, outcome, method, word, length, best_support_size, + worst_support_size, branch_count, homing, generation; + +// Expected row: +// "OK", "PLAN", "PAIR_RESOLUTION", ["to_corridor"], 1, +// 1, 1, 2, true, 1 diff --git a/examples/warehouse/04_explain.cypher b/examples/warehouse/04_explain.cypher new file mode 100644 index 0000000..98376d1 --- /dev/null +++ b/examples/warehouse/04_explain.cypher @@ -0,0 +1,13 @@ +CALL sync.explain_plan( + "warehouse", + 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"] +) +YIELD step, action, predicted_hypotheses, output_trace, branch_hypotheses, generation +RETURN step, action, predicted_hypotheses, output_trace, branch_hypotheses, generation +ORDER BY step, output_trace; + +// Expected: 5 rows. Step 0 has both initial states. Step 1 has one singleton +// branch for west_landmark and one for east_landmark. Step 2 retains those two +// traces, and both branches contain only dock:north. diff --git a/examples/warehouse/05_monitor.cypher b/examples/warehouse/05_monitor.cypher new file mode 100644 index 0000000..f13ea46 --- /dev/null +++ b/examples/warehouse/05_monitor.cypher @@ -0,0 +1,12 @@ +CALL sync.validate_update( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], + 1, + ["corridor_w:east", "corridor_e:west"], + true +) +YIELD status, decision, reason, expected_hypotheses, unexpected_hypotheses, generation +RETURN status, decision, reason, expected_hypotheses, unexpected_hypotheses, generation; + +// Expected decision: "CONTINUE" diff --git a/examples/warehouse/README.md b/examples/warehouse/README.md new file mode 100644 index 0000000..1072de4 --- /dev/null +++ b/examples/warehouse/README.md @@ -0,0 +1,10 @@ +# Warehouse example + +Run the numbered Cypher files in order. The model has five orientation-aware +states, four controller actions, and four abstract sensor outputs. Preparation +validates all 20 transition cells and all 20 observation cells before writing +the generation-1 pair oracle. + +The ambiguous bay hypotheses synchronize at `dock:north` under +`["to_corridor", "go_west"]`. The first action alone emits different landmark +outputs, so `["to_corridor"]` is also a homing disambiguation word. diff --git a/meson.build b/meson.build index ddffeef..b8ee95c 100644 --- a/meson.build +++ b/meson.build @@ -143,6 +143,6 @@ install_data( ) install_subdir( - 'examples/office', + 'examples/warehouse', install_dir: get_option('datadir') / 'sync-kgraph' / 'examples', ) diff --git a/src/memgraph/sync_module.c b/src/memgraph/sync_module.c index 040b31f..a48cda9 100644 --- a/src/memgraph/sync_module.c +++ b/src/memgraph/sync_module.c @@ -9,19 +9,39 @@ #include #include +#define SG_MATERIALIZE_BATCH 512U +#define SG_ERROR_MESSAGE_CAPACITY 192U +#define SG_VALIDATE_REPORTED_ARGUMENT 5U +#define SG_VALIDATE_AVAILABLE_ARGUMENT 6U + +// NOLINTNEXTLINE(misc-use-internal-linkage) int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory); +// NOLINTNEXTLINE(misc-use-internal-linkage) int mgp_shutdown_module(void); +typedef struct { + uint64_t generation; + bool dirty; + bool prepared; +} model_metadata; + +typedef enum { + DOMAIN_STATE = 0, + DOMAIN_ACTION, + DOMAIN_OUTPUT, +} domain_kind; + +typedef struct { + struct mgp_result *result; + const sg_automaton *automaton; + struct mgp_memory *memory; +} explain_context; + static bool mg_ok(enum mgp_error error) { return error == MGP_ERROR_NO_ERROR; } #ifdef SYNC_KGRAPH_MGP_COMPAT -// Compat for Memgraph headers that predate the *_move API and -// mgp_unordered_map_make_empty. The non-move variants copy the value, so -// these shims destroy the source on success to preserve the move-semantics -// contract at the call sites (call sites destroy the value themselves on -// failure). static enum mgp_error sync_compat_list_append_move(struct mgp_list *list, struct mgp_value *value) { const enum mgp_error error = mgp_list_append(list, value); if (error == MGP_ERROR_NO_ERROR) { @@ -48,8 +68,17 @@ static void set_error(struct mgp_result *result, const char *message) { (void)mgp_result_set_error_msg(result, message); } +static void set_status_error(struct mgp_result *result, const char *operation, sg_status status) { + char message[SG_ERROR_MESSAGE_CAPACITY] = {0}; + (void)snprintf(message, sizeof(message), "%s: %s", operation, sg_status_name(status)); + set_error(result, message); +} + static bool insert_value(struct mgp_result_record *record, const char *field, struct mgp_value *value) { + if (value == NULL) { + return false; + } const bool ok = mg_ok(mgp_result_record_insert(record, field, value)); mgp_value_destroy(value); return ok; @@ -57,202 +86,70 @@ static bool insert_value(struct mgp_result_record *record, const char *field, static bool insert_string(struct mgp_result_record *record, const char *field, const char *value, struct mgp_memory *memory) { - struct mgp_value *mg_value = NULL; - if (!mg_ok(mgp_value_make_string(value, memory, &mg_value))) { - return false; - } - return insert_value(record, field, mg_value); + struct mgp_value *created = NULL; + return mg_ok(mgp_value_make_string(value, memory, &created)) && + insert_value(record, field, created); } static bool insert_int(struct mgp_result_record *record, const char *field, int64_t value, struct mgp_memory *memory) { - struct mgp_value *mg_value = NULL; - if (!mg_ok(mgp_value_make_int(value, memory, &mg_value))) { - return false; - } - return insert_value(record, field, mg_value); + struct mgp_value *created = NULL; + return mg_ok(mgp_value_make_int(value, memory, &created)) && insert_value(record, field, created); } static bool insert_bool(struct mgp_result_record *record, const char *field, bool value, struct mgp_memory *memory) { - struct mgp_value *mg_value = NULL; - if (!mg_ok(mgp_value_make_bool(value ? 1 : 0, memory, &mg_value))) { - return false; - } - return insert_value(record, field, mg_value); + struct mgp_value *created = NULL; + return mg_ok(mgp_value_make_bool(value ? 1 : 0, memory, &created)) && + insert_value(record, field, created); } -static bool get_arg(struct mgp_list *args, size_t index, struct mgp_value **value) { - size_t size = 0U; - if (!mg_ok(mgp_list_size(args, &size)) || index >= size) { - return false; - } - return mg_ok(mgp_list_at(args, index, value)) && *value != NULL; +static bool new_record(struct mgp_result *result, struct mgp_result_record **record) { + return mg_ok(mgp_result_new_record(result, record)) && *record != NULL; } -static bool get_arg_string(struct mgp_list *args, size_t index, const char **value) { - struct mgp_value *arg = NULL; - if (!get_arg(args, index, &arg)) { - return false; - } - enum mgp_value_type type = MGP_VALUE_TYPE_NULL; - if (!mg_ok(mgp_value_get_type(arg, &type)) || type != MGP_VALUE_TYPE_STRING) { - return false; - } - return mg_ok(mgp_value_get_string(arg, value)) && *value != NULL; +static bool get_arg(struct mgp_list *arguments, size_t index, struct mgp_value **value) { + size_t count = 0U; + return mg_ok(mgp_list_size(arguments, &count)) && index < count && + mg_ok(mgp_list_at(arguments, index, value)) && *value != NULL; } -static bool get_arg_int_default(struct mgp_list *args, size_t index, int64_t default_value, - int64_t *value) { - struct mgp_value *arg = NULL; - if (!get_arg(args, index, &arg)) { - *value = default_value; - return true; - } - enum mgp_value_type type = MGP_VALUE_TYPE_NULL; - if (!mg_ok(mgp_value_get_type(arg, &type)) || type == MGP_VALUE_TYPE_NULL) { - *value = default_value; - return true; - } - if (type != MGP_VALUE_TYPE_INT) { - return false; - } - return mg_ok(mgp_value_get_int(arg, value)); +static bool get_string_arg(struct mgp_list *arguments, size_t index, const char **value) { + struct mgp_value *argument = NULL; + return get_arg(arguments, index, &argument) && mg_ok(mgp_value_get_string(argument, value)) && + *value != NULL; } -static bool get_arg_bool_default(struct mgp_list *args, size_t index, bool default_value, - bool *value) { - struct mgp_value *arg = NULL; - if (!get_arg(args, index, &arg)) { - *value = default_value; - return true; - } - enum mgp_value_type type = MGP_VALUE_TYPE_NULL; - if (!mg_ok(mgp_value_get_type(arg, &type)) || type == MGP_VALUE_TYPE_NULL) { - *value = default_value; - return true; - } - if (type != MGP_VALUE_TYPE_BOOL) { - return false; - } - int raw = 0; - if (!mg_ok(mgp_value_get_bool(arg, &raw))) { - return false; - } - *value = raw != 0; - return true; +static bool get_int_arg(struct mgp_list *arguments, size_t index, int64_t *value) { + struct mgp_value *argument = NULL; + return get_arg(arguments, index, &argument) && mg_ok(mgp_value_get_int(argument, value)); } -static bool get_arg_string_default(struct mgp_list *args, size_t index, const char *default_value, - const char **value) { - struct mgp_value *arg = NULL; - if (!get_arg(args, index, &arg)) { +static bool get_bool_arg_default(struct mgp_list *arguments, size_t index, bool default_value, + bool *value) { + struct mgp_value *argument = NULL; + if (!get_arg(arguments, index, &argument)) { *value = default_value; return true; } enum mgp_value_type type = MGP_VALUE_TYPE_NULL; - if (!mg_ok(mgp_value_get_type(arg, &type)) || type == MGP_VALUE_TYPE_NULL) { - *value = default_value; - return true; - } - if (type != MGP_VALUE_TYPE_STRING) { + if (!mg_ok(mgp_value_get_type(argument, &type))) { return false; } - return mg_ok(mgp_value_get_string(arg, value)) && *value != NULL; -} - -static bool list_to_ids(const sg_dfa *dfa, struct mgp_value *value, size_t **ids, size_t *count) { - *ids = NULL; - *count = 0U; - - enum mgp_value_type type = MGP_VALUE_TYPE_NULL; - if (!mg_ok(mgp_value_get_type(value, &type)) || type == MGP_VALUE_TYPE_NULL) { + if (type == MGP_VALUE_TYPE_NULL) { + *value = default_value; return true; } - if (type != MGP_VALUE_TYPE_LIST) { - return false; - } - - struct mgp_list *list = NULL; - if (!mg_ok(mgp_value_get_list(value, &list)) || list == NULL) { - return false; - } - size_t size = 0U; - if (!mg_ok(mgp_list_size(list, &size))) { - return false; - } - - size_t *created = calloc(size == 0U ? 1U : size, sizeof(created[0])); - if (created == NULL) { - return false; - } - - for (size_t i = 0U; i < size; ++i) { - struct mgp_value *item = NULL; - const char *key = NULL; - if (!mg_ok(mgp_list_at(list, i, &item)) || item == NULL || - !mg_ok(mgp_value_get_string(item, &key)) || key == NULL || - sg_dfa_find_state(dfa, key, &created[i]) != SG_OK) { - free(created); - return false; - } - } - - *ids = created; - *count = size; - return true; -} - -static bool word_value_to_ids(const sg_dfa *dfa, struct mgp_value *value, sg_word *word) { - if (sg_word_init(word) != SG_OK) { - return false; - } - enum mgp_value_type type = MGP_VALUE_TYPE_NULL; - if (!mg_ok(mgp_value_get_type(value, &type)) || type != MGP_VALUE_TYPE_LIST) { - return false; - } - struct mgp_list *list = NULL; - if (!mg_ok(mgp_value_get_list(value, &list)) || list == NULL) { - return false; - } - size_t size = 0U; - if (!mg_ok(mgp_list_size(list, &size))) { + int raw = 0; + if (type != MGP_VALUE_TYPE_BOOL || !mg_ok(mgp_value_get_bool(argument, &raw))) { return false; } - for (size_t i = 0U; i < size; ++i) { - struct mgp_value *item = NULL; - const char *letter = NULL; - size_t letter_id = 0U; - if (!mg_ok(mgp_list_at(list, i, &item)) || item == NULL || - !mg_ok(mgp_value_get_string(item, &letter)) || letter == NULL || - sg_dfa_find_letter(dfa, letter, &letter_id) != SG_OK || - sg_word_append(word, letter_id) != SG_OK) { - return false; - } - } + *value = raw != 0; return true; } -static struct mgp_map *make_model_params(const char *model, struct mgp_memory *memory) { - struct mgp_map *params = NULL; - struct mgp_value *model_value = NULL; - if (!mg_ok(mgp_unordered_map_make_empty(memory, ¶ms)) || params == NULL) { - return NULL; - } - if (!mg_ok(mgp_value_make_string(model, memory, &model_value)) || model_value == NULL) { - mgp_map_destroy(params); - return NULL; - } - if (!mg_ok(mgp_map_insert_move(params, "model", model_value))) { - mgp_value_destroy(model_value); - mgp_map_destroy(params); - return NULL; - } - return params; -} - static bool params_insert_value(struct mgp_map *params, const char *key, struct mgp_value *value) { - if (params == NULL || key == NULL || value == NULL) { + if (params == NULL || value == NULL) { if (value != NULL) { mgp_value_destroy(value); } @@ -267,42 +164,62 @@ static bool params_insert_value(struct mgp_map *params, const char *key, struct static bool params_insert_string(struct mgp_map *params, const char *key, const char *value, struct mgp_memory *memory) { - struct mgp_value *mg_value = NULL; - if (!mg_ok(mgp_value_make_string(value, memory, &mg_value)) || mg_value == NULL) { - return false; - } - return params_insert_value(params, key, mg_value); + struct mgp_value *created = NULL; + return mg_ok(mgp_value_make_string(value, memory, &created)) && + params_insert_value(params, key, created); } static bool params_insert_int(struct mgp_map *params, const char *key, int64_t value, struct mgp_memory *memory) { - struct mgp_value *mg_value = NULL; - if (!mg_ok(mgp_value_make_int(value, memory, &mg_value)) || mg_value == NULL) { - return false; - } - return params_insert_value(params, key, mg_value); + struct mgp_value *created = NULL; + return mg_ok(mgp_value_make_int(value, memory, &created)) && + params_insert_value(params, key, created); } static bool params_insert_bool(struct mgp_map *params, const char *key, bool value, struct mgp_memory *memory) { - struct mgp_value *mg_value = NULL; - if (!mg_ok(mgp_value_make_bool(value ? 1 : 0, memory, &mg_value)) || mg_value == NULL) { - return false; + struct mgp_value *created = NULL; + return mg_ok(mgp_value_make_bool(value ? 1 : 0, memory, &created)) && + params_insert_value(params, key, created); +} + +static bool map_insert_string(struct mgp_map *map, const char *key, const char *value, + struct mgp_memory *memory) { + return params_insert_string(map, key, value, memory); +} + +static bool map_insert_int(struct mgp_map *map, const char *key, int64_t value, + struct mgp_memory *memory) { + return params_insert_int(map, key, value, memory); +} + +static bool map_insert_bool(struct mgp_map *map, const char *key, bool value, + struct mgp_memory *memory) { + return params_insert_bool(map, key, value, memory); +} + +static struct mgp_map *make_model_params(const char *model, struct mgp_memory *memory) { + struct mgp_map *params = NULL; + if (!mg_ok(mgp_unordered_map_make_empty(memory, ¶ms)) || params == NULL || + !params_insert_string(params, "model", model, memory)) { + if (params != NULL) { + mgp_map_destroy(params); + } + return NULL; } - return params_insert_value(params, key, mg_value); + return params; } -static bool exec_query_drain(struct mgp_graph *graph, struct mgp_memory *memory, const char *query, - struct mgp_map *params) { - struct mgp_execution_result *exec = NULL; - if (!mg_ok(mgp_execute_query(graph, memory, query, params, &exec)) || exec == NULL) { +static bool execute_drain(struct mgp_graph *graph, struct mgp_memory *memory, const char *query, + struct mgp_map *params) { + struct mgp_execution_result *execution = NULL; + if (!mg_ok(mgp_execute_query(graph, memory, query, params, &execution)) || execution == NULL) { return false; } - bool ok = true; for (;;) { struct mgp_map *row = NULL; - if (!mg_ok(mgp_pull_one(exec, graph, memory, &row))) { + if (!mg_ok(mgp_pull_one(execution, graph, memory, &row))) { ok = false; break; } @@ -310,130 +227,144 @@ static bool exec_query_drain(struct mgp_graph *graph, struct mgp_memory *memory, break; } } - - mgp_execution_result_destroy(exec); + mgp_execution_result_destroy(execution); return ok; } -static bool exec_model_query(struct mgp_graph *graph, struct mgp_memory *memory, const char *model, - const char *query) { +static bool execute_model_query(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, const char *query) { struct mgp_map *params = make_model_params(model, memory); if (params == NULL) { return false; } - const bool ok = exec_query_drain(graph, memory, query, params); + const bool ok = execute_drain(graph, memory, query, params); mgp_map_destroy(params); return ok; } -static int64_t size_to_int64(size_t value) { - return (value > (size_t)INT64_MAX) ? INT64_MAX : (int64_t)value; +static bool row_string(struct mgp_map *row, const char *field, const char **value) { + struct mgp_value *entry = NULL; + return mg_ok(mgp_map_at(row, field, &entry)) && entry != NULL && + mg_ok(mgp_value_get_string(entry, value)) && *value != NULL; } -static char *join_state_keys(const sg_dfa *dfa, const size_t *states, size_t state_count) { - if (dfa == NULL || (states == NULL && state_count != 0U)) { - return NULL; - } - size_t length = 0U; - for (size_t i = 0U; i < state_count; ++i) { - const char *key = sg_dfa_state_key(dfa, states[i]); - if (key == NULL) { - return NULL; - } - const size_t add = strlen(key) + (i == 0U ? 0U : 1U); - if (add > SIZE_MAX - length) { - return NULL; - } - length += add; +static bool row_int(struct mgp_map *row, const char *field, int64_t *value) { + struct mgp_value *entry = NULL; + return mg_ok(mgp_map_at(row, field, &entry)) && entry != NULL && + mg_ok(mgp_value_get_int(entry, value)); +} + +static bool row_bool(struct mgp_map *row, const char *field, bool *value) { + struct mgp_value *entry = NULL; + int raw = 0; + if (!mg_ok(mgp_map_at(row, field, &entry)) || entry == NULL || + !mg_ok(mgp_value_get_bool(entry, &raw))) { + return false; } - if (length == SIZE_MAX) { - return NULL; + *value = raw != 0; + return true; +} + +static bool uint64_to_int64(uint64_t value, int64_t *converted) { + if (value > (uint64_t)INT64_MAX) { + return false; } + *converted = (int64_t)value; + return true; +} - char *joined = malloc(length + 1U); - if (joined == NULL) { - return NULL; +static bool size_to_int64(size_t value, int64_t *converted) { + if (value > (size_t)INT64_MAX) { + return false; } - char *cursor = joined; - for (size_t i = 0U; i < state_count; ++i) { - const char *key = sg_dfa_state_key(dfa, states[i]); - if (i != 0U) { - *cursor = ','; - ++cursor; - } - const size_t key_len = strlen(key); - memcpy(cursor, key, key_len); - cursor += key_len; + *converted = (int64_t)value; + return true; +} + +static int64_t index_to_int64(size_t value) { + int64_t converted = -1; + return value == SG_INDEX_NONE || !size_to_int64(value, &converted) ? -1 : converted; +} + +static bool int64_to_index(int64_t value, size_t *converted) { + if (value < -1 || (uint64_t)value > (uint64_t)SIZE_MAX) { + return false; } - *cursor = '\0'; - return joined; + *converted = value == -1 ? SG_INDEX_NONE : (size_t)value; + return true; } -static char *join_word_letters(const sg_dfa *dfa, const sg_word *word) { - if (dfa == NULL || word == NULL) { - return NULL; +static sg_status load_metadata(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, model_metadata *metadata) { + static const char *query = "MATCH (m:SyncModel {model: $model}) " + "RETURN coalesce(m.generation, 0) AS generation, " + "coalesce(m.dirty, true) AS dirty, " + "coalesce(m.prepared_generation, -1) AS prepared_generation"; + struct mgp_map *params = make_model_params(model, memory); + if (params == NULL) { + return SG_ERR_ALLOC; } - size_t length = 0U; - for (size_t i = 0U; i < word->length; ++i) { - const char *key = sg_dfa_letter_key(dfa, word->letters[i]); - if (key == NULL) { - return NULL; - } - const size_t add = strlen(key) + (i == 0U ? 0U : 1U); - if (add > SIZE_MAX - length) { - return NULL; - } - length += add; + struct mgp_execution_result *execution = NULL; + sg_status status = SG_OK; + if (!mg_ok(mgp_execute_query(graph, memory, query, params, &execution)) || execution == NULL) { + status = SG_ERR_INVALID_MODEL; } - if (length == SIZE_MAX) { - return NULL; + struct mgp_map *row = NULL; + int64_t generation = -1; + int64_t prepared_generation = -1; + bool dirty = true; + if (status == SG_OK && + (!mg_ok(mgp_pull_one(execution, graph, memory, &row)) || row == NULL || + !row_int(row, "generation", &generation) || !row_bool(row, "dirty", &dirty) || + !row_int(row, "prepared_generation", &prepared_generation) || generation < 0)) { + status = SG_ERR_NOT_FOUND; + } + struct mgp_map *extra = NULL; + if (status == SG_OK && + (!mg_ok(mgp_pull_one(execution, graph, memory, &extra)) || extra != NULL)) { + status = SG_ERR_INVALID_MODEL; } - - char *joined = malloc(length + 1U); - if (joined == NULL) { - return NULL; + if (status == SG_OK) { + metadata->generation = (uint64_t)generation; + metadata->dirty = dirty; + metadata->prepared = !dirty && prepared_generation == generation; } - char *cursor = joined; - for (size_t i = 0U; i < word->length; ++i) { - const char *key = sg_dfa_letter_key(dfa, word->letters[i]); - if (i != 0U) { - *cursor = ','; - ++cursor; - } - const size_t key_len = strlen(key); - memcpy(cursor, key, key_len); - cursor += key_len; + if (execution != NULL) { + mgp_execution_result_destroy(execution); } - *cursor = '\0'; - return joined; + mgp_map_destroy(params); + return status; } -static bool row_string(struct mgp_map *row, const char *field, const char **value) { - struct mgp_value *field_value = NULL; - if (!mg_ok(mgp_map_at(row, field, &field_value)) || field_value == NULL) { - return false; - } - return mg_ok(mgp_value_get_string(field_value, value)) && *value != NULL; +static sg_status add_domain_value(sg_automaton_builder *builder, domain_kind kind, + const char *value) { + switch (kind) { + case DOMAIN_STATE: + return sg_automaton_builder_add_state(builder, value); + case DOMAIN_ACTION: + return sg_automaton_builder_add_action(builder, value); + case DOMAIN_OUTPUT: + return sg_automaton_builder_add_output(builder, value); + } + return SG_ERR_INVALID_ARGUMENT; } -static bool exec_add_single_field(struct mgp_graph *graph, struct mgp_memory *memory, - const char *model, const char *query, const char *field, - sg_dfa_builder *builder, bool add_state) { +static sg_status load_domain(struct mgp_graph *graph, struct mgp_memory *memory, const char *model, + const char *query, const char *field, domain_kind kind, + sg_automaton_builder *builder) { struct mgp_map *params = make_model_params(model, memory); if (params == NULL) { - return false; + return SG_ERR_ALLOC; } - struct mgp_execution_result *exec = NULL; - if (!mg_ok(mgp_execute_query(graph, memory, query, params, &exec)) || exec == NULL) { - mgp_map_destroy(params); - return false; + struct mgp_execution_result *execution = NULL; + sg_status status = SG_OK; + if (!mg_ok(mgp_execute_query(graph, memory, query, params, &execution)) || execution == NULL) { + status = SG_ERR_INVALID_MODEL; } - - bool ok = true; - for (;;) { + while (status == SG_OK) { struct mgp_map *row = NULL; - if (!mg_ok(mgp_pull_one(exec, graph, memory, &row))) { - ok = false; + if (!mg_ok(mgp_pull_one(execution, graph, memory, &row))) { + status = SG_ERR_INVALID_MODEL; break; } if (row == NULL) { @@ -441,914 +372,1219 @@ static bool exec_add_single_field(struct mgp_graph *graph, struct mgp_memory *me } const char *value = NULL; if (!row_string(row, field, &value)) { - ok = false; - break; - } - sg_status status = add_state ? sg_dfa_builder_add_state(builder, value) - : sg_dfa_builder_add_letter(builder, value); - if (status != SG_OK) { - ok = false; + status = SG_ERR_INVALID_MODEL; break; } + status = add_domain_value(builder, kind, value); + } + if (execution != NULL) { + mgp_execution_result_destroy(execution); } - - mgp_execution_result_destroy(exec); mgp_map_destroy(params); - return ok; + return status; } -static bool exec_add_transitions(struct mgp_graph *graph, struct mgp_memory *memory, - const char *model, sg_dfa_builder *builder) { +static sg_status load_transitions(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, sg_automaton_builder *builder) { static const char *query = - "MATCH (src:SyncState {model: $model})-[t:SYNC_TRANS {model: $model}]->" - "(dst:SyncState {model: $model}) " - "RETURN src.state_key AS source, t.letter AS letter, dst.state_key AS target " - "ORDER BY source, letter, target"; + "MATCH (src:SyncState {model: $model})-" + "[t:SYNC_TRANS {model: $model}]->(dst:SyncState {model: $model}) " + "RETURN src.state_key AS source, t.action_key AS action, dst.state_key AS target " + "ORDER BY source, action, target"; struct mgp_map *params = make_model_params(model, memory); if (params == NULL) { - return false; + return SG_ERR_ALLOC; } - struct mgp_execution_result *exec = NULL; - if (!mg_ok(mgp_execute_query(graph, memory, query, params, &exec)) || exec == NULL) { - mgp_map_destroy(params); - return false; + struct mgp_execution_result *execution = NULL; + sg_status status = SG_OK; + if (!mg_ok(mgp_execute_query(graph, memory, query, params, &execution)) || execution == NULL) { + status = SG_ERR_INVALID_MODEL; } - - bool ok = true; - for (;;) { + while (status == SG_OK) { struct mgp_map *row = NULL; - if (!mg_ok(mgp_pull_one(exec, graph, memory, &row))) { - ok = false; + if (!mg_ok(mgp_pull_one(execution, graph, memory, &row))) { + status = SG_ERR_INVALID_MODEL; break; } if (row == NULL) { break; } const char *source = NULL; - const char *letter = NULL; + const char *action = NULL; const char *target = NULL; - if (!row_string(row, "source", &source) || !row_string(row, "letter", &letter) || - !row_string(row, "target", &target) || - sg_dfa_builder_add_transition(builder, source, letter, target) != SG_OK) { - ok = false; + if (!row_string(row, "source", &source) || !row_string(row, "action", &action) || + !row_string(row, "target", &target)) { + status = SG_ERR_INVALID_MODEL; break; } + status = sg_automaton_builder_add_transition(builder, source, action, target); } + if (execution != NULL) { + mgp_execution_result_destroy(execution); + } + mgp_map_destroy(params); + return status; +} - mgp_execution_result_destroy(exec); +static sg_status load_observations(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, sg_automaton_builder *builder) { + static const char *query = + "MATCH (src:SyncState {model: $model})-" + "[o:SYNC_OBS {model: $model}]->(output:SyncOutput {model: $model}) " + "RETURN src.state_key AS source, o.action_key AS action, output.output_key AS output " + "ORDER BY source, action, output"; + struct mgp_map *params = make_model_params(model, memory); + if (params == NULL) { + return SG_ERR_ALLOC; + } + struct mgp_execution_result *execution = NULL; + sg_status status = SG_OK; + if (!mg_ok(mgp_execute_query(graph, memory, query, params, &execution)) || execution == NULL) { + status = SG_ERR_INVALID_MODEL; + } + while (status == SG_OK) { + struct mgp_map *row = NULL; + if (!mg_ok(mgp_pull_one(execution, graph, memory, &row))) { + status = SG_ERR_INVALID_MODEL; + break; + } + if (row == NULL) { + break; + } + const char *source = NULL; + const char *action = NULL; + const char *output = NULL; + if (!row_string(row, "source", &source) || !row_string(row, "action", &action) || + !row_string(row, "output", &output)) { + status = SG_ERR_INVALID_MODEL; + break; + } + status = sg_automaton_builder_add_observation(builder, source, action, output); + } + if (execution != NULL) { + mgp_execution_result_destroy(execution); + } mgp_map_destroy(params); - return ok; + return status; } -static sg_status load_model(struct mgp_graph *graph, struct mgp_memory *memory, const char *model, - bool complete_with_sink, sg_dfa **dfa) { +static sg_status load_automaton(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, model_metadata *metadata, + sg_automaton **automaton) { static const char *state_query = "MATCH (s:SyncState {model: $model}) RETURN s.state_key AS state_key " "ORDER BY s.state_id, s.state_key"; - static const char *letter_query = - "MATCH (l:SyncLetter {model: $model}) RETURN l.letter AS letter " - "ORDER BY l.letter_id, l.letter"; + static const char *action_query = + "MATCH (a:SyncAction {model: $model}) RETURN a.action_key AS action_key " + "ORDER BY a.action_id, a.action_key"; + static const char *output_query = + "MATCH (o:SyncOutput {model: $model}) RETURN o.output_key AS output_key " + "ORDER BY o.output_id, o.output_key"; + + sg_status status = load_metadata(graph, memory, model, metadata); + sg_automaton_builder *builder = NULL; + if (status == SG_OK) { + status = sg_automaton_builder_init(&builder); + } + if (status == SG_OK) { + status = load_domain(graph, memory, model, state_query, "state_key", DOMAIN_STATE, builder); + } + if (status == SG_OK) { + status = load_domain(graph, memory, model, action_query, "action_key", DOMAIN_ACTION, builder); + } + if (status == SG_OK) { + status = load_domain(graph, memory, model, output_query, "output_key", DOMAIN_OUTPUT, builder); + } + if (status == SG_OK) { + status = load_transitions(graph, memory, model, builder); + } + if (status == SG_OK) { + status = load_observations(graph, memory, model, builder); + } + if (status == SG_OK) { + status = sg_automaton_builder_build(builder, metadata->generation, automaton); + } + sg_automaton_builder_free(builder); + return status; +} - sg_dfa_builder *builder = NULL; - sg_status status = sg_dfa_builder_init(&builder); - if (status != SG_OK) { - return status; +static bool pair_count_for_states(size_t state_count, size_t *pair_count) { + if (state_count == SIZE_MAX) { + return false; } - if (!exec_add_single_field(graph, memory, model, state_query, "state_key", builder, true) || - !exec_add_single_field(graph, memory, model, letter_query, "letter", builder, false) || - !exec_add_transitions(graph, memory, model, builder)) { - sg_dfa_builder_free(builder); - return SG_ERR_INVALID_ARGUMENT; + const size_t second = state_count + 1U; + if (state_count != 0U && second > SIZE_MAX / state_count) { + return false; } + *pair_count = (state_count * second) / 2U; + return true; +} - status = sg_dfa_builder_build(builder, complete_with_sink, dfa); - sg_dfa_builder_free(builder); +static sg_status load_oracle(struct mgp_graph *graph, struct mgp_memory *memory, const char *model, + const sg_automaton *automaton, sg_pair_oracle **oracle) { + static const char *query = + "MATCH (p:SyncPair {model: $model, generation: $generation}) " + "RETURN p.pair_id AS pair_id, p.mergeable AS mergeable, " + "p.merge_distance AS merge_distance, p.merge_action_id AS merge_action_id, " + "p.merge_next_pair AS merge_next_pair, p.resolvable AS resolvable, " + "p.resolution_distance AS resolution_distance, " + "p.resolution_action_id AS resolution_action_id, " + "p.resolution_next_pair AS resolution_next_pair ORDER BY pair_id"; + size_t expected_count = 0U; + if (!pair_count_for_states(sg_automaton_state_count(automaton), &expected_count)) { + return SG_ERR_ALLOC; + } + if (expected_count == 0U) { + return SG_ERR_INVALID_MODEL; + } + sg_pair_record *records = calloc(expected_count, sizeof(*records)); + if (records == NULL) { + return SG_ERR_ALLOC; + } + struct mgp_map *params = make_model_params(model, memory); + int64_t generation = 0; + if (params == NULL || !uint64_to_int64(sg_automaton_generation(automaton), &generation) || + !params_insert_int(params, "generation", generation, memory)) { + if (params != NULL) { + mgp_map_destroy(params); + } + free(records); + return SG_ERR_ALLOC; + } + struct mgp_execution_result *execution = NULL; + sg_status status = SG_OK; + if (!mg_ok(mgp_execute_query(graph, memory, query, params, &execution)) || execution == NULL) { + status = SG_ERR_INVALID_MODEL; + } + size_t count = 0U; + while (status == SG_OK) { + struct mgp_map *row = NULL; + if (!mg_ok(mgp_pull_one(execution, graph, memory, &row))) { + status = SG_ERR_INVALID_MODEL; + break; + } + if (row == NULL) { + break; + } + if (count >= expected_count) { + status = SG_ERR_INVALID_MODEL; + break; + } + int64_t pair = -1; + int64_t merge_distance = -1; + int64_t merge_action = -1; + int64_t merge_next = -1; + int64_t resolution_distance = -1; + int64_t resolution_action = -1; + int64_t resolution_next = -1; + bool mergeable = false; + bool resolvable = false; + if (!row_int(row, "pair_id", &pair) || !row_bool(row, "mergeable", &mergeable) || + !row_int(row, "merge_distance", &merge_distance) || + !row_int(row, "merge_action_id", &merge_action) || + !row_int(row, "merge_next_pair", &merge_next) || + !row_bool(row, "resolvable", &resolvable) || + !row_int(row, "resolution_distance", &resolution_distance) || + !row_int(row, "resolution_action_id", &resolution_action) || + !row_int(row, "resolution_next_pair", &resolution_next) || pair < 0 || + !int64_to_index(merge_distance, &records[count].merge_distance) || + !int64_to_index(merge_action, &records[count].merge_action) || + !int64_to_index(merge_next, &records[count].merge_next_pair) || + !int64_to_index(resolution_distance, &records[count].resolution_distance) || + !int64_to_index(resolution_action, &records[count].resolution_action) || + !int64_to_index(resolution_next, &records[count].resolution_next_pair)) { + status = SG_ERR_INVALID_MODEL; + break; + } + records[count].pair = (size_t)pair; + records[count].mergeable = mergeable; + records[count].resolvable = resolvable; + ++count; + } + if (execution != NULL) { + mgp_execution_result_destroy(execution); + } + mgp_map_destroy(params); + if (status == SG_OK) { + status = sg_pair_oracle_restore(automaton, records, count, oracle); + } + free(records); return status; } -static bool insert_word(struct mgp_result_record *record, const sg_dfa *dfa, const sg_word *word, - struct mgp_memory *memory) { +static bool list_to_ids(const sg_automaton *automaton, struct mgp_value *value, bool actions, + bool allow_empty, size_t **ids, size_t *count) { + *ids = NULL; + *count = 0U; + struct mgp_list *list = NULL; + if (!mg_ok(mgp_value_get_list(value, &list)) || list == NULL || + !mg_ok(mgp_list_size(list, count)) || (!allow_empty && *count == 0U)) { + return false; + } + size_t *created = calloc(*count == 0U ? 1U : *count, sizeof(*created)); + if (created == NULL) { + return false; + } + for (size_t index = 0U; index < *count; ++index) { + struct mgp_value *item = NULL; + const char *key = NULL; + if (!mg_ok(mgp_list_at(list, index, &item)) || item == NULL || + !mg_ok(mgp_value_get_string(item, &key)) || key == NULL) { + free(created); + return false; + } + const sg_status status = actions ? sg_automaton_find_action(automaton, key, &created[index]) + : sg_automaton_find_state(automaton, key, &created[index]); + if (status != SG_OK) { + free(created); + return false; + } + } + *ids = created; + return true; +} + +static bool argument_to_ids(const sg_automaton *automaton, struct mgp_list *arguments, size_t index, + bool actions, bool allow_empty, size_t **ids, size_t *count) { + struct mgp_value *value = NULL; + return get_arg(arguments, index, &value) && + list_to_ids(automaton, value, actions, allow_empty, ids, count); +} + +static bool argument_to_word(const sg_automaton *automaton, struct mgp_list *arguments, + size_t index, sg_word *word) { + size_t *actions = NULL; + size_t count = 0U; + if (!argument_to_ids(automaton, arguments, index, true, true, &actions, &count) || + sg_word_init(word) != SG_OK) { + free(actions); + return false; + } + bool ok = true; + for (size_t position = 0U; position < count; ++position) { + if (sg_word_append(word, actions[position]) != SG_OK) { + ok = false; + break; + } + } + free(actions); + if (!ok) { + sg_word_free(word); + } + return ok; +} + +static struct mgp_value *make_key_list(const sg_automaton *automaton, const size_t *ids, + size_t count, bool outputs, struct mgp_memory *memory) { + struct mgp_list *list = NULL; + if (!mg_ok(mgp_list_make_empty(count, memory, &list)) || list == NULL) { + return NULL; + } + for (size_t index = 0U; index < count; ++index) { + const char *key = outputs ? sg_automaton_output_key(automaton, ids[index]) + : sg_automaton_state_key(automaton, ids[index]); + struct mgp_value *item = NULL; + if (key == NULL || !mg_ok(mgp_value_make_string(key, memory, &item)) || item == NULL || + !mg_ok(mgp_list_append_move(list, item))) { + if (item != NULL) { + mgp_value_destroy(item); + } + mgp_list_destroy(list); + return NULL; + } + } + struct mgp_value *value = NULL; + if (!mg_ok(mgp_value_make_list(list, &value)) || value == NULL) { + mgp_list_destroy(list); + return NULL; + } + return value; +} + +static bool insert_key_list(struct mgp_result_record *record, const char *field, + const sg_automaton *automaton, const size_t *ids, size_t count, + bool outputs, struct mgp_memory *memory) { + return insert_value(record, field, make_key_list(automaton, ids, count, outputs, memory)); +} + +static bool insert_empty_list(struct mgp_result_record *record, const char *field, + struct mgp_memory *memory) { + struct mgp_list *list = NULL; + if (!mg_ok(mgp_list_make_empty(0U, memory, &list)) || list == NULL) { + return false; + } + struct mgp_value *value = NULL; + if (!mg_ok(mgp_value_make_list(list, &value)) || value == NULL) { + mgp_list_destroy(list); + return false; + } + return insert_value(record, field, value); +} + +static bool insert_word(struct mgp_result_record *record, const sg_automaton *automaton, + const sg_word *word, struct mgp_memory *memory) { struct mgp_list *list = NULL; if (!mg_ok(mgp_list_make_empty(word->length, memory, &list)) || list == NULL) { return false; } - for (size_t i = 0U; i < word->length; ++i) { - struct mgp_value *value = NULL; - if (!mg_ok(mgp_value_make_string(sg_dfa_letter_key(dfa, word->letters[i]), memory, &value)) || - value == NULL) { - mgp_list_destroy(list); - return false; - } - if (!mg_ok(mgp_list_append_move(list, value))) { - mgp_value_destroy(value); + for (size_t index = 0U; index < word->length; ++index) { + const char *key = sg_automaton_action_key(automaton, word->actions[index]); + struct mgp_value *item = NULL; + if (key == NULL || !mg_ok(mgp_value_make_string(key, memory, &item)) || item == NULL || + !mg_ok(mgp_list_append_move(list, item))) { + if (item != NULL) { + mgp_value_destroy(item); + } mgp_list_destroy(list); return false; } } - struct mgp_value *list_value = NULL; - if (!mg_ok(mgp_value_make_list(list, &list_value)) || list_value == NULL) { + struct mgp_value *value = NULL; + if (!mg_ok(mgp_value_make_list(list, &value)) || value == NULL) { mgp_list_destroy(list); return false; } - return insert_value(record, "word", list_value); + return insert_value(record, "word", value); } -static bool new_record(struct mgp_result *result, struct mgp_result_record **record) { - return mg_ok(mgp_result_new_record(result, record)) && *record != NULL; +static const char *outcome_reason(sg_plan_outcome outcome) { + switch (outcome) { + case SG_OUTCOME_PLAN: + return "plan found"; + case SG_OUTCOME_ALREADY_SATISFIED: + return "objective already satisfied"; + case SG_OUTCOME_NO_PLAN: + return "no qualifying word exists"; + case SG_OUTCOME_RESOURCE_BOUND: + return "search budget exhausted"; + } + return "unknown outcome"; } -static bool create_pair_node(struct mgp_graph *graph, struct mgp_memory *memory, const char *model, - const sg_dfa *dfa, const sg_pair_oracle *oracle, size_t pair) { - static const char *query = - "CREATE (:SyncPair {model: $model, pair_id: $pair_id, first_key: $first_key, " - "second_key: $second_key, distance: $distance, has_witness: $has_witness, " - "witness: $witness, next_pair: $next_pair, generation: 0}) " - "RETURN 1 AS ok"; - +static bool append_pair_record(struct mgp_list *list, const sg_automaton *automaton, + const sg_pair_oracle *oracle, size_t pair, + struct mgp_memory *memory) { + sg_pair_record record = {0}; size_t first = 0U; size_t second = 0U; - bool has_witness = false; - size_t distance = 0U; - size_t witness_letter = 0U; - size_t next_pair = 0U; - if (sg_pair_oracle_pair_states(oracle, pair, &first, &second) != SG_OK || - sg_pair_oracle_pair_witness(oracle, pair, &has_witness, &distance, &witness_letter, - &next_pair) != SG_OK) { + if (sg_pair_oracle_record(oracle, pair, &record) != SG_OK || + sg_pair_oracle_pair_states(oracle, pair, &first, &second) != SG_OK) { return false; } + const char *merge_action = record.merge_action == SG_INDEX_NONE + ? "" + : sg_automaton_action_key(automaton, record.merge_action); + const char *resolution_action = + record.resolution_action == SG_INDEX_NONE + ? "" + : sg_automaton_action_key(automaton, record.resolution_action); + int64_t pair_id = 0; + int64_t first_id = 0; + int64_t second_id = 0; + if (merge_action == NULL || resolution_action == NULL || !size_to_int64(pair, &pair_id) || + !size_to_int64(first, &first_id) || !size_to_int64(second, &second_id)) { + return false; + } + struct mgp_map *map = NULL; + if (!mg_ok(mgp_unordered_map_make_empty(memory, &map)) || map == NULL) { + return false; + } + const bool populated = + map_insert_int(map, "pair_id", pair_id, memory) && + map_insert_string(map, "first_key", sg_automaton_state_key(automaton, first), memory) && + map_insert_int(map, "first_id", first_id, memory) && + map_insert_string(map, "second_key", sg_automaton_state_key(automaton, second), memory) && + map_insert_int(map, "second_id", second_id, memory) && + map_insert_bool(map, "mergeable", record.mergeable, memory) && + map_insert_int(map, "merge_distance", index_to_int64(record.merge_distance), memory) && + map_insert_string(map, "merge_action", merge_action, memory) && + map_insert_int(map, "merge_action_id", index_to_int64(record.merge_action), memory) && + map_insert_int(map, "merge_next_pair", index_to_int64(record.merge_next_pair), memory) && + map_insert_bool(map, "resolvable", record.resolvable, memory) && + map_insert_int(map, "resolution_distance", index_to_int64(record.resolution_distance), + memory) && + map_insert_string(map, "resolution_action", resolution_action, memory) && + map_insert_int(map, "resolution_action_id", index_to_int64(record.resolution_action), + memory) && + map_insert_int(map, "resolution_next_pair", index_to_int64(record.resolution_next_pair), + memory); + if (!populated) { + mgp_map_destroy(map); + return false; + } + struct mgp_value *value = NULL; + if (!mg_ok(mgp_value_make_map(map, &value)) || value == NULL) { + mgp_map_destroy(map); + return false; + } + if (!mg_ok(mgp_list_append_move(list, value))) { + mgp_value_destroy(value); + return false; + } + return true; +} - const char *first_key = sg_dfa_state_key(dfa, first); - const char *second_key = sg_dfa_state_key(dfa, second); - const char *witness = (has_witness && witness_letter < sg_dfa_letter_count(dfa)) - ? sg_dfa_letter_key(dfa, witness_letter) - : ""; - const int64_t distance_value = has_witness ? size_to_int64(distance) : -1; - const int64_t next_pair_value = (has_witness && next_pair < sg_pair_oracle_pair_count(oracle)) - ? size_to_int64(next_pair) - : -1; - +static bool execute_pair_batch(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, const sg_automaton *automaton, + const sg_pair_oracle *oracle, size_t first_pair, size_t pair_count) { + static const char *query = + "UNWIND $records AS r " + "CREATE (:SyncPair {model: $model, generation: $generation, pair_id: r.pair_id, " + "first_key: r.first_key, first_id: r.first_id, second_key: r.second_key, " + "second_id: r.second_id, mergeable: r.mergeable, merge_distance: r.merge_distance, " + "merge_action: r.merge_action, merge_action_id: r.merge_action_id, " + "merge_next_pair: r.merge_next_pair, resolvable: r.resolvable, " + "resolution_distance: r.resolution_distance, resolution_action: r.resolution_action, " + "resolution_action_id: r.resolution_action_id, " + "resolution_next_pair: r.resolution_next_pair})"; + struct mgp_list *records = NULL; + if (!mg_ok(mgp_list_make_empty(pair_count, memory, &records)) || records == NULL) { + return false; + } + bool ok = true; + for (size_t offset = 0U; ok && offset < pair_count; ++offset) { + ok = append_pair_record(records, automaton, oracle, first_pair + offset, memory); + } + struct mgp_value *records_value = NULL; + if (ok) { + ok = mg_ok(mgp_value_make_list(records, &records_value)) && records_value != NULL; + } + if (!ok) { + mgp_list_destroy(records); + return false; + } struct mgp_map *params = make_model_params(model, memory); - if (params == NULL) { + int64_t generation = 0; + if (params == NULL || !uint64_to_int64(sg_automaton_generation(automaton), &generation) || + !params_insert_int(params, "generation", generation, memory) || + !params_insert_value(params, "records", records_value)) { + if (params != NULL) { + mgp_map_destroy(params); + } return false; } - const bool ok = params_insert_int(params, "pair_id", size_to_int64(pair), memory) && - params_insert_string(params, "first_key", first_key, memory) && - params_insert_string(params, "second_key", second_key, memory) && - params_insert_int(params, "distance", distance_value, memory) && - params_insert_bool(params, "has_witness", has_witness, memory) && - params_insert_string(params, "witness", witness, memory) && - params_insert_int(params, "next_pair", next_pair_value, memory) && - exec_query_drain(graph, memory, query, params); + ok = execute_drain(graph, memory, query, params); mgp_map_destroy(params); return ok; } -static bool create_pair_edge(struct mgp_graph *graph, struct mgp_memory *memory, const char *model, - const sg_dfa *dfa, const sg_pair_oracle *oracle, size_t pair, - size_t letter) { - static const char *query = - "MATCH (p:SyncPair {model: $model, pair_id: $pair_id}), " - "(n:SyncPair {model: $model, pair_id: $next_pair}) " - "CREATE (p)-[:PAIR_NEXT {model: $model, letter: $letter, letter_id: $letter_id}]->(n) " - "CREATE (n)-[:PAIR_PRE {model: $model, letter: $letter, letter_id: $letter_id}]->(p) " - "RETURN 1 AS ok"; - +static bool append_pair_edge(struct mgp_list *list, const sg_automaton *automaton, + const sg_pair_oracle *oracle, size_t edge, struct mgp_memory *memory) { + const size_t action_count = sg_automaton_action_count(automaton); + const size_t pair = edge / action_count; + const size_t action = edge % action_count; size_t next_pair = 0U; - if (sg_pair_oracle_pair_next(oracle, pair, letter, &next_pair) != SG_OK) { + bool outputs_differ = false; + if (sg_pair_oracle_pair_step(oracle, pair, action, &next_pair, &outputs_differ) != SG_OK) { + return false; + } + int64_t pair_id = 0; + int64_t action_id = 0; + int64_t next_pair_id = 0; + if (!size_to_int64(pair, &pair_id) || !size_to_int64(action, &action_id) || + !size_to_int64(next_pair, &next_pair_id)) { + return false; + } + struct mgp_map *map = NULL; + if (!mg_ok(mgp_unordered_map_make_empty(memory, &map)) || map == NULL) { return false; } - const char *letter_key = sg_dfa_letter_key(dfa, letter); - if (letter_key == NULL) { + const bool populated = + map_insert_int(map, "pair_id", pair_id, memory) && + map_insert_string(map, "action", sg_automaton_action_key(automaton, action), memory) && + map_insert_int(map, "action_id", action_id, memory) && + map_insert_int(map, "next_pair", next_pair_id, memory) && + map_insert_bool(map, "outputs_differ", outputs_differ, memory); + if (!populated) { + mgp_map_destroy(map); return false; } + struct mgp_value *value = NULL; + if (!mg_ok(mgp_value_make_map(map, &value)) || value == NULL) { + mgp_map_destroy(map); + return false; + } + if (!mg_ok(mgp_list_append_move(list, value))) { + mgp_value_destroy(value); + return false; + } + return true; +} +static bool execute_edge_batch(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, const sg_automaton *automaton, + const sg_pair_oracle *oracle, size_t first_edge, size_t edge_count) { + static const char *query = + "UNWIND $edges AS e " + "MATCH (p:SyncPair {model: $model, generation: $generation, pair_id: e.pair_id}), " + "(n:SyncPair {model: $model, generation: $generation, pair_id: e.next_pair}) " + "CREATE (p)-[:PAIR_NEXT {model: $model, generation: $generation, action: e.action, " + "action_id: e.action_id, outputs_differ: e.outputs_differ}]->(n) " + "CREATE (n)-[:PAIR_PRE {model: $model, generation: $generation, action: e.action, " + "action_id: e.action_id, outputs_differ: e.outputs_differ}]->(p)"; + struct mgp_list *edges = NULL; + if (!mg_ok(mgp_list_make_empty(edge_count, memory, &edges)) || edges == NULL) { + return false; + } + bool ok = true; + for (size_t offset = 0U; ok && offset < edge_count; ++offset) { + ok = append_pair_edge(edges, automaton, oracle, first_edge + offset, memory); + } + struct mgp_value *edges_value = NULL; + if (ok) { + ok = mg_ok(mgp_value_make_list(edges, &edges_value)) && edges_value != NULL; + } + if (!ok) { + mgp_list_destroy(edges); + return false; + } struct mgp_map *params = make_model_params(model, memory); - if (params == NULL) { + int64_t generation = 0; + if (params == NULL || !uint64_to_int64(sg_automaton_generation(automaton), &generation) || + !params_insert_int(params, "generation", generation, memory) || + !params_insert_value(params, "edges", edges_value)) { + if (params != NULL) { + mgp_map_destroy(params); + } return false; } - const bool ok = params_insert_int(params, "pair_id", size_to_int64(pair), memory) && - params_insert_int(params, "next_pair", size_to_int64(next_pair), memory) && - params_insert_string(params, "letter", letter_key, memory) && - params_insert_int(params, "letter_id", size_to_int64(letter), memory) && - exec_query_drain(graph, memory, query, params); + ok = execute_drain(graph, memory, query, params); mgp_map_destroy(params); return ok; } -static bool materialize_pair_oracle(struct mgp_graph *graph, struct mgp_memory *memory, - const char *model, const sg_dfa *dfa, - const sg_pair_oracle *oracle) { - static const char *clear_query = - "MATCH (p:SyncPair {model: $model}) DETACH DELETE p RETURN count(p) AS removed"; - if (!exec_model_query(graph, memory, model, clear_query)) { +static bool materialize_oracle(struct mgp_graph *graph, struct mgp_memory *memory, + const char *model, const sg_automaton *automaton, + const sg_pair_oracle *oracle, bool materialize_edges) { + static const char *clear_query = "MATCH (p:SyncPair {model: $model}) DETACH DELETE p"; + static const char *finish_query = "MATCH (m:SyncModel {model: $model}) " + "SET m.dirty = false, m.prepared_generation = $generation, " + "m.pair_edges_materialized = $materialize_edges"; + if (!execute_model_query(graph, memory, model, clear_query)) { return false; } - const size_t pair_count = sg_pair_oracle_pair_count(oracle); - const size_t letter_count = sg_dfa_letter_count(dfa); - for (size_t pair = 0U; pair < pair_count; ++pair) { - if (!create_pair_node(graph, memory, model, dfa, oracle, pair)) { + for (size_t first = 0U; first < pair_count; first += SG_MATERIALIZE_BATCH) { + const size_t remaining = pair_count - first; + const size_t count = remaining < SG_MATERIALIZE_BATCH ? remaining : SG_MATERIALIZE_BATCH; + if (!execute_pair_batch(graph, memory, model, automaton, oracle, first, count)) { return false; } } - for (size_t pair = 0U; pair < pair_count; ++pair) { - for (size_t letter = 0U; letter < letter_count; ++letter) { - if (!create_pair_edge(graph, memory, model, dfa, oracle, pair, letter)) { + if (materialize_edges) { + const size_t edge_count = sg_pair_oracle_pair_edge_count(oracle); + for (size_t first = 0U; first < edge_count; first += SG_MATERIALIZE_BATCH) { + const size_t remaining = edge_count - first; + const size_t count = remaining < SG_MATERIALIZE_BATCH ? remaining : SG_MATERIALIZE_BATCH; + if (!execute_edge_batch(graph, memory, model, automaton, oracle, first, count)) { return false; } } } - return true; -} - -typedef struct { - struct mgp_graph *graph; - struct mgp_memory *memory; - const char *model; - const sg_dfa *dfa; - const char *mode; - const char *target_key; -} subset_materialize_ctx; - -static bool clear_subset_cache(struct mgp_graph *graph, struct mgp_memory *memory, - const char *model, const char *mode, const char *target_key) { - static const char *query = - "MATCH (s:SyncSubset {model: $model, mode: $mode, target_key: $target_key}) " - "DETACH DELETE s RETURN count(s) AS removed"; struct mgp_map *params = make_model_params(model, memory); - if (params == NULL) { + int64_t generation = 0; + if (params == NULL || !uint64_to_int64(sg_automaton_generation(automaton), &generation) || + !params_insert_int(params, "generation", generation, memory) || + !params_insert_bool(params, "materialize_edges", materialize_edges, memory)) { + if (params != NULL) { + mgp_map_destroy(params); + } return false; } - const bool ok = params_insert_string(params, "mode", mode, memory) && - params_insert_string(params, "target_key", target_key, memory) && - exec_query_drain(graph, memory, query, params); + const bool ok = execute_drain(graph, memory, finish_query, params); mgp_map_destroy(params); return ok; } -static bool create_subset_node(const subset_materialize_ctx *ctx, const size_t *states, - size_t state_count, const sg_word *word) { - static const char *query = - "MERGE (s:SyncSubset {model: $model, mode: $mode, target_key: $target_key, " - "subset_key: $subset_key}) " - "SET s.word = $word, s.size = $size, s.word_length = $word_length, s.generation = 0 " - "RETURN 1 AS ok"; - - char *subset_key = join_state_keys(ctx->dfa, states, state_count); - char *word_key = join_word_letters(ctx->dfa, word); - if (subset_key == NULL || word_key == NULL) { - free(subset_key); - free(word_key); +static bool runtime_load(struct mgp_graph *graph, struct mgp_memory *memory, const char *model, + sg_automaton **automaton, sg_pair_oracle **oracle, + struct mgp_result *result) { + model_metadata metadata = {0}; + sg_status status = load_automaton(graph, memory, model, &metadata, automaton); + if (status != SG_OK) { + set_status_error(result, "model extraction failed", status); return false; } - - struct mgp_map *params = make_model_params(ctx->model, ctx->memory); - if (params == NULL) { - free(subset_key); - free(word_key); + if (!metadata.prepared) { + set_error(result, "model is dirty or has not been prepared"); + sg_automaton_free(*automaton); + *automaton = NULL; return false; } - const bool ok = - params_insert_string(params, "mode", ctx->mode, ctx->memory) && - params_insert_string(params, "target_key", ctx->target_key, ctx->memory) && - params_insert_string(params, "subset_key", subset_key, ctx->memory) && - params_insert_string(params, "word", word_key, ctx->memory) && - params_insert_int(params, "size", size_to_int64(state_count), ctx->memory) && - params_insert_int(params, "word_length", size_to_int64(word->length), ctx->memory) && - exec_query_drain(ctx->graph, ctx->memory, query, params); - mgp_map_destroy(params); - free(subset_key); - free(word_key); - return ok; -} - -static sg_status materialize_subset_visit(void *raw_ctx, const size_t *states, size_t state_count, - const sg_word *word) { - const subset_materialize_ctx *ctx = raw_ctx; - if (ctx == NULL || word == NULL || (states == NULL && state_count != 0U)) { - return SG_ERR_INVALID_ARGUMENT; + status = load_oracle(graph, memory, model, *automaton, oracle); + if (status != SG_OK) { + set_status_error(result, "prepared oracle is invalid", status); + sg_automaton_free(*automaton); + *automaton = NULL; + return false; } - return create_subset_node(ctx, states, state_count, word) ? SG_OK : SG_ERR_INVALID_ARGUMENT; + return true; } -static void validate_model_cb(struct mgp_list *args, struct mgp_graph *graph, - struct mgp_result *result, struct mgp_memory *memory) { +static void prepare_model_cb(struct mgp_list *arguments, struct mgp_graph *graph, + struct mgp_result *result, struct mgp_memory *memory) { const char *model = NULL; - if (!get_arg_string(args, 0U, &model)) { - set_error(result, "validate_model requires a string model argument"); + bool materialize_edges = false; + if (!get_string_arg(arguments, 0U, &model) || + !get_bool_arg_default(arguments, 1U, false, &materialize_edges)) { + set_error(result, "expected model and optional materialize_pair_edges boolean"); return; } - sg_dfa *dfa = NULL; - const sg_status status = load_model(graph, memory, model, false, &dfa); - - struct mgp_result_record *record = NULL; - if (!new_record(result, &record)) { - sg_dfa_free(dfa); - set_error(result, "failed to create result record"); + model_metadata metadata = {0}; + sg_automaton *automaton = NULL; + sg_status status = load_automaton(graph, memory, model, &metadata, &automaton); + if (status != SG_OK) { + set_status_error(result, "model extraction failed", status); return; } - - const bool ok = status == SG_OK; - if (!insert_bool(record, "ok", ok, memory) || - !insert_string(record, "code", sg_status_name(status), memory) || - !insert_string(record, "message", - ok ? "model is complete and deterministic" : sg_status_name(status), memory) || - !insert_int(record, "states", ok ? (int64_t)sg_dfa_state_count(dfa) : 0, memory) || - !insert_int(record, "letters", ok ? (int64_t)sg_dfa_letter_count(dfa) : 0, memory) || - !insert_int(record, "transitions", ok ? (int64_t)sg_dfa_transition_count(dfa) : 0, memory)) { - sg_dfa_free(dfa); - set_error(result, "failed to insert validation result"); - return; + sg_pair_oracle *oracle = NULL; + status = sg_pair_oracle_build(automaton, &oracle); + const size_t pair_count = status == SG_OK ? sg_pair_oracle_pair_count(oracle) : 0U; + const size_t edge_count = status == SG_OK ? sg_pair_oracle_pair_edge_count(oracle) : 0U; + int64_t ignored = 0; + if (status == SG_OK && (!size_to_int64(pair_count, &ignored) || + !size_to_int64(sg_automaton_action_count(automaton), &ignored))) { + status = SG_ERR_RESOURCE_BOUND; + } + if (status == SG_OK && + !materialize_oracle(graph, memory, model, automaton, oracle, materialize_edges)) { + status = SG_ERR_INVALID_MODEL; } - sg_dfa_free(dfa); -} - -static void build_model_cb(struct mgp_list *args, struct mgp_graph *graph, - struct mgp_result *result, struct mgp_memory *memory) { - const char *model = NULL; - bool complete_with_sink = false; - if (!get_arg_string(args, 0U, &model) || - !get_arg_bool_default(args, 1U, false, &complete_with_sink)) { - set_error(result, "build_model requires model and optional complete_with_sink"); + if (status != SG_OK) { + set_status_error(result, "model preparation failed", status); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); return; } - sg_dfa *dfa = NULL; - const sg_status status = load_model(graph, memory, model, complete_with_sink, &dfa); struct mgp_result_record *record = NULL; - if (!new_record(result, &record)) { - sg_dfa_free(dfa); - set_error(result, "failed to create result record"); - return; - } - const bool ok = status == SG_OK; - if (!insert_string(record, "status", sg_status_name(status), memory) || - !insert_int(record, "generation", 0, memory) || - !insert_int(record, "states", ok ? (int64_t)sg_dfa_state_count(dfa) : 0, memory) || - !insert_int(record, "letters", ok ? (int64_t)sg_dfa_letter_count(dfa) : 0, memory) || - !insert_int(record, "transitions", ok ? (int64_t)sg_dfa_transition_count(dfa) : 0, memory)) { - sg_dfa_free(dfa); - set_error(result, "failed to insert build result"); - return; + int64_t generation = 0; + int64_t states = 0; + int64_t actions = 0; + int64_t outputs = 0; + int64_t transitions = 0; + int64_t pairs = 0; + int64_t edges = 0; + const bool converted = uint64_to_int64(sg_automaton_generation(automaton), &generation) && + size_to_int64(sg_automaton_state_count(automaton), &states) && + size_to_int64(sg_automaton_action_count(automaton), &actions) && + size_to_int64(sg_automaton_output_count(automaton), &outputs) && + size_to_int64(sg_automaton_transition_count(automaton), &transitions) && + size_to_int64(pair_count, &pairs) && size_to_int64(edge_count, &edges); + if (!converted || !new_record(result, &record) || + !insert_string(record, "status", "OK", memory) || + !insert_int(record, "generation", generation, memory) || + !insert_int(record, "states", states, memory) || + !insert_int(record, "actions", actions, memory) || + !insert_int(record, "outputs", outputs, memory) || + !insert_int(record, "transitions", transitions, memory) || + !insert_int(record, "pairs", pairs, memory) || + !insert_int(record, "pair_edges", edges, memory) || + !insert_int(record, "mergeable_pairs", (int64_t)sg_pair_oracle_mergeable_pair_count(oracle), + memory) || + !insert_int(record, "resolvable_pairs", (int64_t)sg_pair_oracle_resolvable_pair_count(oracle), + memory) || + !insert_bool(record, "materialized_pair_edges", materialize_edges, memory)) { + set_error(result, "failed to create preparation result"); } - sg_dfa_free(dfa); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); } -static void build_pair_oracle_cb(struct mgp_list *args, struct mgp_graph *graph, - struct mgp_result *result, struct mgp_memory *memory) { +static bool insert_plan_common(struct mgp_result_record *record, const sg_automaton *automaton, + const sg_plan_result *plan, struct mgp_memory *memory) { + int64_t generation = 0; + int64_t length = 0; + int64_t expansions = 0; + int64_t planning_time = 0; + return uint64_to_int64(plan->generation, &generation) && + size_to_int64(plan->word.length, &length) && + size_to_int64(plan->expansions, &expansions) && + uint64_to_int64(plan->planning_time_us, &planning_time) && + insert_string(record, "status", "OK", memory) && + insert_string(record, "outcome", sg_plan_outcome_name(plan->outcome), memory) && + insert_string(record, "reason", outcome_reason(plan->outcome), memory) && + insert_string(record, "method", sg_plan_method_name(plan->method), memory) && + insert_word(record, automaton, &plan->word, memory) && + insert_int(record, "length", length, memory) && + insert_int(record, "expansions", expansions, memory) && + insert_int(record, "generation", generation, memory) && + insert_int(record, "planning_time_us", planning_time, memory); +} + +static void plan_sync_cb(struct mgp_list *arguments, struct mgp_graph *graph, + struct mgp_result *result, struct mgp_memory *memory) { const char *model = NULL; - bool materialize = true; - if (!get_arg_string(args, 0U, &model) || !get_arg_bool_default(args, 1U, true, &materialize)) { - set_error(result, "build_pair_oracle requires model and optional materialize flag"); + int64_t budget = 0; + if (!get_string_arg(arguments, 0U, &model) || !get_int_arg(arguments, 2U, &budget) || + budget <= 0 || (uint64_t)budget > (uint64_t)SIZE_MAX) { + set_error(result, "expected model, nonempty hypotheses, and positive budget"); return; } - sg_dfa *dfa = NULL; + sg_automaton *automaton = NULL; sg_pair_oracle *oracle = NULL; - sg_status status = load_model(graph, memory, model, false, &dfa); - if (status == SG_OK) { - status = sg_pair_oracle_build(dfa, &oracle); - } - if (status == SG_OK && materialize && - !materialize_pair_oracle(graph, memory, model, dfa, oracle)) { - status = SG_ERR_INVALID_ARGUMENT; - } - - struct mgp_result_record *record = NULL; - if (!new_record(result, &record)) { - sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); - set_error(result, "failed to create result record"); + if (!runtime_load(graph, memory, model, &automaton, &oracle, result)) { return; } - if (!insert_string(record, "status", sg_status_name(status), memory) || - !insert_int(record, "pairs", oracle == NULL ? 0 : (int64_t)sg_pair_oracle_pair_count(oracle), - memory) || - !insert_int(record, "pair_edges", - oracle == NULL ? 0 : (int64_t)sg_pair_oracle_pair_edge_count(oracle), memory) || - !insert_int(record, "mergeable_pairs", - oracle == NULL ? 0 : (int64_t)sg_pair_oracle_mergeable_pair_count(oracle), - memory) || - !insert_bool(record, "materialized", status == SG_OK && materialize, memory) || - !insert_int(record, "generation", 0, memory)) { + size_t *hypotheses = NULL; + size_t hypothesis_count = 0U; + if (!argument_to_ids(automaton, arguments, 1U, false, false, &hypotheses, &hypothesis_count)) { + set_error(result, "hypotheses must be a nonempty list of prepared state keys"); sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); - set_error(result, "failed to insert oracle result"); + sg_automaton_free(automaton); return; } + sg_plan_result plan = {0}; + const sg_status status = + sg_plan_sync(automaton, oracle, hypotheses, hypothesis_count, (size_t)budget, &plan); + free(hypotheses); + if (status != SG_OK) { + set_status_error(result, "synchronization planning failed", status); + } else { + struct mgp_result_record *record = NULL; + int64_t final_support = 0; + const char *final_state = plan.final_state == SG_INDEX_NONE + ? "" + : sg_automaton_state_key(automaton, plan.final_state); + if (final_state == NULL || !size_to_int64(plan.final_support_size, &final_support) || + !new_record(result, &record) || !insert_plan_common(record, automaton, &plan, memory) || + !insert_string(record, "final_state_key", final_state, memory) || + !insert_int(record, "final_support_size", final_support, memory)) { + set_error(result, "failed to create synchronization result"); + } + } + sg_plan_result_free(&plan); sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); + sg_automaton_free(automaton); } -static void word_for_set_impl(struct mgp_list *args, struct mgp_graph *graph, - struct mgp_result *result, struct mgp_memory *memory, - bool target_required) { +static void plan_disambiguate_cb(struct mgp_list *arguments, struct mgp_graph *graph, + struct mgp_result *result, struct mgp_memory *memory) { const char *model = NULL; - const char *mode_name = target_required ? "REACH_AND_SYNC" : "SYNC"; + int64_t bound = 0; int64_t budget = 0; - if (!get_arg_string(args, 0U, &model) || - !get_arg_string_default(args, 3U, mode_name, &mode_name) || - !get_arg_int_default(args, 4U, 0, &budget)) { - set_error(result, "invalid word_for_set arguments"); + if (!get_string_arg(arguments, 0U, &model) || !get_int_arg(arguments, 2U, &bound) || + !get_int_arg(arguments, 3U, &budget) || bound <= 0 || budget <= 0 || + (uint64_t)bound > (uint64_t)SIZE_MAX || (uint64_t)budget > (uint64_t)SIZE_MAX) { + set_error(result, "expected model, nonempty hypotheses, positive bound, and positive budget"); return; } - sg_mode mode = SG_MODE_SYNC; - if (sg_mode_parse(mode_name, &mode) != SG_OK || budget < 0) { - set_error(result, "invalid mode or budget"); - return; - } - - sg_dfa *dfa = NULL; + sg_automaton *automaton = NULL; sg_pair_oracle *oracle = NULL; - sg_status status = load_model(graph, memory, model, false, &dfa); - if (status == SG_OK) { - status = sg_pair_oracle_build(dfa, &oracle); - } - if (status != SG_OK) { - struct mgp_result_record *record = NULL; - if (new_record(result, &record)) { - (void)insert_string(record, "status", sg_status_name(status), memory); - (void)insert_int(record, "length", 0, memory); - (void)insert_string(record, "final_state_key", "", memory); - sg_word empty = {0}; - (void)sg_word_init(&empty); - (void)insert_word(record, dfa, &empty, memory); - } - sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); + if (!runtime_load(graph, memory, model, &automaton, &oracle, result)) { return; } - - struct mgp_value *state_arg = NULL; - struct mgp_value *target_arg = NULL; - if (!get_arg(args, 1U, &state_arg) || !get_arg(args, 2U, &target_arg)) { - set_error(result, "state_keys and target_keys list arguments are required"); + size_t *hypotheses = NULL; + size_t hypothesis_count = 0U; + if (!argument_to_ids(automaton, arguments, 1U, false, false, &hypotheses, &hypothesis_count)) { + set_error(result, "hypotheses must be a nonempty list of prepared state keys"); sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); + sg_automaton_free(automaton); return; } - - size_t *states = NULL; - size_t *targets = NULL; - size_t state_count = 0U; - size_t target_count = 0U; - if (!list_to_ids(dfa, state_arg, &states, &state_count) || - !list_to_ids(dfa, target_arg, &targets, &target_count) || - (target_required && target_count == 0U)) { - free(states); - free(targets); - set_error(result, "state_keys and target_keys must be lists of known state keys"); - sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); - return; + sg_plan_result plan = {0}; + const sg_status status = sg_plan_disambiguate(automaton, oracle, hypotheses, hypothesis_count, + (size_t)bound, (size_t)budget, &plan); + free(hypotheses); + if (status != SG_OK) { + set_status_error(result, "disambiguation planning failed", status); + } else { + struct mgp_result_record *record = NULL; + int64_t best = 0; + int64_t worst = 0; + int64_t branches = 0; + if (!size_to_int64(plan.best_support_size, &best) || + !size_to_int64(plan.worst_support_size, &worst) || + !size_to_int64(plan.branch_count, &branches) || !new_record(result, &record) || + !insert_plan_common(record, automaton, &plan, memory) || + !insert_int(record, "best_support_size", best, memory) || + !insert_int(record, "worst_support_size", worst, memory) || + !insert_int(record, "branch_count", branches, memory) || + !insert_bool(record, "homing", plan.homing, memory)) { + set_error(result, "failed to create disambiguation result"); + } } - - sg_word_result word_result = {0}; - status = sg_word_for_set(dfa, oracle, states, state_count, targets, target_count, mode, - (size_t)budget, &word_result); - - struct mgp_result_record *record = NULL; - if (!new_record(result, &record) || - !insert_string(record, "status", - status == SG_OK ? sg_result_kind_name(word_result.kind) - : sg_status_name(status), - memory) || - !insert_word(record, dfa, &word_result.word, memory) || - !insert_int(record, "length", (int64_t)word_result.word.length, memory) || - !insert_string(record, "final_state_key", - word_result.final_state == (size_t)-1 - ? "" - : sg_dfa_state_key(dfa, word_result.final_state), - memory) || - !insert_int(record, "generation", 0, memory)) { - set_error(result, "failed to insert word result"); - } - - sg_word_result_free(&word_result); - free(states); - free(targets); + sg_plan_result_free(&plan); sg_pair_oracle_free(oracle); - sg_dfa_free(dfa); -} - -static void word_for_set_cb(struct mgp_list *args, struct mgp_graph *graph, - struct mgp_result *result, struct mgp_memory *memory) { - word_for_set_impl(args, graph, result, memory, false); + sg_automaton_free(automaton); } -static void word_to_target_cb(struct mgp_list *args, struct mgp_graph *graph, - struct mgp_result *result, struct mgp_memory *memory) { - word_for_set_impl(args, graph, result, memory, true); +static sg_status explain_visit(void *raw_context, size_t step, size_t action, + const size_t *predicted_states, size_t predicted_count, + const size_t *output_trace, size_t trace_length, + const size_t *branch_states, size_t branch_count) { + explain_context *context = raw_context; + struct mgp_result_record *record = NULL; + int64_t step_value = 0; + int64_t generation = 0; + const char *action_key = + action == SG_INDEX_NONE ? "" : sg_automaton_action_key(context->automaton, action); + if (action_key == NULL || !size_to_int64(step, &step_value) || + !uint64_to_int64(sg_automaton_generation(context->automaton), &generation) || + !new_record(context->result, &record) || + !insert_int(record, "step", step_value, context->memory) || + !insert_string(record, "action", action_key, context->memory) || + !insert_key_list(record, "predicted_hypotheses", context->automaton, predicted_states, + predicted_count, false, context->memory) || + !insert_key_list(record, "output_trace", context->automaton, output_trace, trace_length, true, + context->memory) || + !insert_key_list(record, "branch_hypotheses", context->automaton, branch_states, branch_count, + false, context->memory) || + !insert_int(record, "generation", generation, context->memory)) { + return SG_ERR_ALLOC; + } + return SG_OK; } -static void expand_cache_cb(struct mgp_list *args, struct mgp_graph *graph, +static void explain_plan_cb(struct mgp_list *arguments, struct mgp_graph *graph, struct mgp_result *result, struct mgp_memory *memory) { const char *model = NULL; - const char *mode_name = "REACH_AND_SYNC"; - int64_t budget = 0; - if (!get_arg_string(args, 0U, &model) || - !get_arg_string_default(args, 2U, mode_name, &mode_name) || - !get_arg_int_default(args, 3U, 0, &budget) || budget < 0) { - set_error(result, "expand_cache requires model, target_keys, optional mode, optional budget"); - return; - } - - sg_mode mode = SG_MODE_REACH_AND_SYNC; - if (sg_mode_parse(mode_name, &mode) != SG_OK) { - set_error(result, "invalid expand_cache mode"); - return; - } - - sg_dfa *dfa = NULL; - sg_status status = load_model(graph, memory, model, false, &dfa); - if (status != SG_OK) { - struct mgp_result_record *record = NULL; - if (new_record(result, &record)) { - (void)insert_string(record, "status", sg_status_name(status), memory); - (void)insert_int(record, "expanded", 0, memory); - (void)insert_int(record, "cache_size", 0, memory); - } - return; - } - - struct mgp_value *target_arg = NULL; - size_t *targets = NULL; - size_t target_count = 0U; - if (!get_arg(args, 1U, &target_arg) || !list_to_ids(dfa, target_arg, &targets, &target_count)) { - free(targets); - sg_dfa_free(dfa); - set_error(result, "target_keys must be a list of known state keys"); + int64_t generation = -1; + if (!get_string_arg(arguments, 0U, &model) || !get_int_arg(arguments, 1U, &generation) || + generation < 0) { + set_error(result, "expected model, generation, hypotheses, and word"); return; } - - const char *canonical_mode = sg_mode_name(mode); - char *target_key = join_state_keys(dfa, targets, target_count); - if (target_key == NULL) { - free(targets); - sg_dfa_free(dfa); - set_error(result, "failed to build target cache key"); + sg_automaton *automaton = NULL; + sg_pair_oracle *oracle = NULL; + if (!runtime_load(graph, memory, model, &automaton, &oracle, result)) { return; } - if (!clear_subset_cache(graph, memory, model, canonical_mode, target_key)) { - free(target_key); - free(targets); - sg_dfa_free(dfa); - set_error(result, "failed to clear subset cache"); + size_t *hypotheses = NULL; + size_t hypothesis_count = 0U; + sg_word word = {0}; + if (!argument_to_ids(automaton, arguments, 2U, false, false, &hypotheses, &hypothesis_count) || + !argument_to_word(automaton, arguments, 3U, &word)) { + set_error(result, "hypotheses or word contain unknown prepared keys"); + free(hypotheses); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); return; } - - subset_materialize_ctx ctx = { - .graph = graph, + explain_context context = { + .result = result, + .automaton = automaton, .memory = memory, - .model = model, - .dfa = dfa, - .mode = canonical_mode, - .target_key = target_key, }; - size_t expanded = 0U; - size_t cache_size = 0U; - status = sg_expand_cache_visit(dfa, targets, target_count, mode, (size_t)budget, - materialize_subset_visit, &ctx, &expanded, &cache_size); + const sg_status status = sg_explain_plan(automaton, (uint64_t)generation, hypotheses, + hypothesis_count, &word, explain_visit, &context); + if (status != SG_OK) { + set_status_error(result, "plan explanation failed", status); + } + sg_word_free(&word); + free(hypotheses); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); +} + +static bool insert_monitor_record(struct mgp_result *result, const sg_automaton *automaton, + const sg_monitor_result *monitor, const char *status, + const char *reason, struct mgp_memory *memory) { + struct mgp_result_record *record = NULL; + int64_t generation = 0; + return uint64_to_int64(monitor->generation, &generation) && new_record(result, &record) && + insert_string(record, "status", status, memory) && + insert_string(record, "decision", sg_monitor_decision_name(monitor->decision), memory) && + insert_string(record, "reason", reason, memory) && + insert_key_list(record, "expected_hypotheses", automaton, monitor->expected_states, + monitor->expected_count, false, memory) && + insert_key_list(record, "unexpected_hypotheses", automaton, monitor->unexpected_states, + monitor->unexpected_count, false, memory) && + insert_int(record, "generation", generation, memory); +} +static bool insert_stale_monitor(struct mgp_result *result, uint64_t generation, + struct mgp_memory *memory) { struct mgp_result_record *record = NULL; - if (!new_record(result, &record) || - !insert_string(record, "status", sg_status_name(status), memory) || - !insert_int(record, "expanded", (int64_t)expanded, memory) || - !insert_int(record, "cache_size", (int64_t)cache_size, memory)) { - set_error(result, "failed to insert cache result"); + int64_t converted = 0; + if (!uint64_to_int64(generation, &converted)) { + return false; } - free(target_key); - free(targets); - sg_dfa_free(dfa); + return new_record(result, &record) && insert_string(record, "status", "OK", memory) && + insert_string(record, "decision", "STALE_GENERATION", memory) && + insert_string(record, "reason", "plan generation does not match model", memory) && + insert_empty_list(record, "expected_hypotheses", memory) && + insert_empty_list(record, "unexpected_hypotheses", memory) && + insert_int(record, "generation", converted, memory); } -static void explain_cb(struct mgp_list *args, struct mgp_graph *graph, struct mgp_result *result, - struct mgp_memory *memory) { +static const char *monitor_reason(sg_monitor_decision decision) { + switch (decision) { + case SG_MONITOR_CONTINUE: + return "reported support matches prediction"; + case SG_MONITOR_REPLAN: + return "reported support is a strict subset"; + case SG_MONITOR_MODEL_VIOLATION: + return "reported support contains unexpected states"; + case SG_MONITOR_STALE_GENERATION: + return "plan generation does not match model"; + case SG_MONITOR_WAIT: + return "localizer report unavailable"; + } + return "unknown monitor decision"; +} + +static void validate_update_cb(struct mgp_list *arguments, struct mgp_graph *graph, + struct mgp_result *result, struct mgp_memory *memory) { const char *model = NULL; - if (!get_arg_string(args, 0U, &model)) { - set_error(result, "explain requires model, state_keys, word"); + int64_t plan_generation = -1; + int64_t completed_steps = -1; + bool localizer_available = true; + if (!get_string_arg(arguments, 0U, &model) || !get_int_arg(arguments, 1U, &plan_generation) || + !get_int_arg(arguments, 4U, &completed_steps) || plan_generation < 0 || completed_steps < 0 || + !get_bool_arg_default(arguments, SG_VALIDATE_AVAILABLE_ARGUMENT, true, + &localizer_available)) { + set_error(result, "invalid validation arguments"); return; } - sg_dfa *dfa = NULL; - sg_status status = load_model(graph, memory, model, false, &dfa); + model_metadata metadata = {0}; + sg_status status = load_metadata(graph, memory, model, &metadata); if (status != SG_OK) { - set_error(result, sg_status_name(status)); + set_status_error(result, "model metadata lookup failed", status); return; } - - struct mgp_value *state_arg = NULL; - struct mgp_value *word_arg = NULL; - size_t *states = NULL; - size_t state_count = 0U; - sg_word word = {0}; - if (!get_arg(args, 1U, &state_arg) || !get_arg(args, 2U, &word_arg) || - !list_to_ids(dfa, state_arg, &states, &state_count) || - !word_value_to_ids(dfa, word_arg, &word)) { - free(states); - sg_word_free(&word); - sg_dfa_free(dfa); - set_error(result, "state_keys and word must be lists of known keys"); + if ((uint64_t)plan_generation != metadata.generation) { + if (!insert_stale_monitor(result, metadata.generation, memory)) { + set_error(result, "failed to create stale-generation result"); + } return; } - - size_t *steps = NULL; - size_t *counts = NULL; - size_t step_count = 0U; - status = sg_explain_word(dfa, states, state_count, &word, &steps, &counts, &step_count); - if (status != SG_OK) { - free(states); + sg_automaton *automaton = NULL; + sg_pair_oracle *oracle = NULL; + if (!runtime_load(graph, memory, model, &automaton, &oracle, result)) { + return; + } + size_t *hypotheses = NULL; + size_t hypothesis_count = 0U; + size_t *reported = NULL; + size_t reported_count = 0U; + sg_word word = {0}; + if (!argument_to_ids(automaton, arguments, 2U, false, false, &hypotheses, &hypothesis_count) || + !argument_to_word(automaton, arguments, 3U, &word) || + !argument_to_ids(automaton, arguments, SG_VALIDATE_REPORTED_ARGUMENT, false, + !localizer_available, &reported, &reported_count) || + (uint64_t)completed_steps > (uint64_t)SIZE_MAX) { + set_error(result, "hypotheses, word, step, or report is invalid for the prepared model"); sg_word_free(&word); - sg_dfa_free(dfa); - set_error(result, sg_status_name(status)); + free(hypotheses); + free(reported); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); return; } - - for (size_t step = 0U; step < step_count; ++step) { - struct mgp_result_record *record = NULL; - if (!new_record(result, &record) || !insert_int(record, "step", (int64_t)step, memory)) { - set_error(result, "failed to insert explain record"); - break; - } - - const char *letter = ""; - if (step > 0U) { - letter = sg_dfa_letter_key(dfa, word.letters[step - 1U]); - } - if (!insert_string(record, "letter", letter, memory)) { - set_error(result, "failed to insert explain letter"); - break; - } - - struct mgp_list *active = NULL; - if (!mg_ok(mgp_list_make_empty(counts[step], memory, &active)) || active == NULL) { - set_error(result, "failed to create active list"); - break; - } - for (size_t i = 0U; i < counts[step]; ++i) { - const size_t state = steps[(step * sg_dfa_state_count(dfa)) + i]; - struct mgp_value *value = NULL; - if (!mg_ok(mgp_value_make_string(sg_dfa_state_key(dfa, state), memory, &value)) || - value == NULL || !mg_ok(mgp_list_append_move(active, value))) { - mgp_value_destroy(value); - mgp_list_destroy(active); - set_error(result, "failed to append active state"); - active = NULL; - break; - } - } - if (active == NULL) { - break; - } - struct mgp_value *active_value = NULL; - if (!mg_ok(mgp_value_make_list(active, &active_value)) || active_value == NULL || - !insert_value(record, "active_state_keys", active_value)) { - mgp_list_destroy(active); - set_error(result, "failed to insert active states"); - break; + sg_monitor_result monitor = {0}; + status = sg_validate_update(automaton, (uint64_t)plan_generation, hypotheses, hypothesis_count, + &word, (size_t)completed_steps, reported, reported_count, + localizer_available, &monitor); + if (status != SG_OK) { + set_status_error(result, "update validation failed", status); + } else { + const char *reason = monitor_reason(monitor.decision); + if (!insert_monitor_record(result, automaton, &monitor, "OK", reason, memory)) { + set_error(result, "failed to create update-validation result"); } } - - sg_explain_free(steps, counts); - free(states); + sg_monitor_result_free(&monitor); sg_word_free(&word); - sg_dfa_free(dfa); + free(hypotheses); + free(reported); + sg_pair_oracle_free(oracle); + sg_automaton_free(automaton); } -static void mark_dirty_cb(struct mgp_list *args, struct mgp_graph *graph, struct mgp_result *result, - struct mgp_memory *memory) { - const char *model = NULL; - if (!get_arg_string(args, 0U, &model)) { - set_error(result, "mark_dirty requires a string model argument"); - return; - } +static void mark_dirty_cb(struct mgp_list *arguments, struct mgp_graph *graph, + struct mgp_result *result, struct mgp_memory *memory) { static const char *query = "MATCH (m:SyncModel {model: $model}) " "SET m.dirty = true, m.generation = coalesce(m.generation, 0) + 1 " "RETURN m.generation AS generation"; + const char *model = NULL; + if (!get_string_arg(arguments, 0U, &model)) { + set_error(result, "expected model"); + return; + } struct mgp_map *params = make_model_params(model, memory); - struct mgp_execution_result *exec = NULL; - if (params == NULL || !mg_ok(mgp_execute_query(graph, memory, query, params, &exec)) || - exec == NULL) { - mgp_map_destroy(params); + struct mgp_execution_result *execution = NULL; + if (params == NULL || !mg_ok(mgp_execute_query(graph, memory, query, params, &execution)) || + execution == NULL) { + if (params != NULL) { + mgp_map_destroy(params); + } set_error(result, "failed to mark model dirty"); return; } struct mgp_map *row = NULL; - int64_t generation = 0; - if (mg_ok(mgp_pull_one(exec, graph, memory, &row)) && row != NULL) { - struct mgp_value *generation_value = NULL; - if (mg_ok(mgp_map_at(row, "generation", &generation_value)) && generation_value != NULL) { - (void)mgp_value_get_int(generation_value, &generation); + int64_t generation = -1; + if (!mg_ok(mgp_pull_one(execution, graph, memory, &row)) || row == NULL || + !row_int(row, "generation", &generation)) { + set_error(result, "model was not found"); + } else { + struct mgp_result_record *record = NULL; + if (!new_record(result, &record) || !insert_string(record, "status", "DIRTY", memory) || + !insert_int(record, "generation", generation, memory)) { + set_error(result, "failed to create dirty-model result"); } } - struct mgp_result_record *record = NULL; - if (!new_record(result, &record) || !insert_string(record, "status", "DIRTY", memory) || - !insert_int(record, "generation", generation, memory)) { - set_error(result, "failed to insert dirty result"); - } - mgp_execution_result_destroy(exec); + mgp_execution_result_destroy(execution); mgp_map_destroy(params); } -static void on_transition_delta_cb(struct mgp_list *args, struct mgp_graph *graph, - struct mgp_result *result, struct mgp_memory *memory) { - mark_dirty_cb(args, graph, result, memory); -} - -static bool add_result(struct mgp_proc *proc, const char *name, struct mgp_type *type) { - return mg_ok(mgp_proc_add_result(proc, name, type)); +static bool add_result(struct mgp_proc *procedure, const char *name, struct mgp_type *type) { + return mg_ok(mgp_proc_add_result(procedure, name, type)); } -static bool add_required(struct mgp_proc *proc, const char *name, struct mgp_type *type) { - return mg_ok(mgp_proc_add_arg(proc, name, type)); +static bool add_required(struct mgp_proc *procedure, const char *name, struct mgp_type *type) { + return mg_ok(mgp_proc_add_arg(procedure, name, type)); } -static bool add_optional_string(struct mgp_proc *proc, const char *name, const char *default_value, - struct mgp_memory *memory, struct mgp_type *string_type) { - struct mgp_value *value = NULL; - if (!mg_ok(mgp_value_make_string(default_value, memory, &value)) || value == NULL) { - return false; - } - const bool ok = mg_ok(mgp_proc_add_opt_arg(proc, name, string_type, value)); - mgp_value_destroy(value); - return ok; -} - -static bool add_optional_int(struct mgp_proc *proc, const char *name, int64_t default_value, - struct mgp_memory *memory, struct mgp_type *int_type) { - struct mgp_value *value = NULL; - if (!mg_ok(mgp_value_make_int(default_value, memory, &value)) || value == NULL) { - return false; - } - const bool ok = mg_ok(mgp_proc_add_opt_arg(proc, name, int_type, value)); - mgp_value_destroy(value); - return ok; -} - -static bool add_optional_bool(struct mgp_proc *proc, const char *name, bool default_value, - struct mgp_memory *memory, struct mgp_type *bool_type) { +static bool add_optional_bool(struct mgp_proc *procedure, const char *name, bool default_value, + struct mgp_memory *memory, struct mgp_type *type) { struct mgp_value *value = NULL; if (!mg_ok(mgp_value_make_bool(default_value ? 1 : 0, memory, &value)) || value == NULL) { return false; } - const bool ok = mg_ok(mgp_proc_add_opt_arg(proc, name, bool_type, value)); - mgp_value_destroy(value); - return ok; -} - -static bool add_optional_empty_list(struct mgp_proc *proc, const char *name, - struct mgp_memory *memory, struct mgp_type *list_type) { - struct mgp_list *list = NULL; - if (!mg_ok(mgp_list_make_empty(0U, memory, &list)) || list == NULL) { - return false; - } - struct mgp_value *value = NULL; - if (!mg_ok(mgp_value_make_list(list, &value)) || value == NULL) { - mgp_list_destroy(list); - return false; - } - const bool ok = mg_ok(mgp_proc_add_opt_arg(proc, name, list_type, value)); + const bool ok = mg_ok(mgp_proc_add_opt_arg(procedure, name, type, value)); mgp_value_destroy(value); return ok; } -static bool register_validate(struct mgp_module *module, struct mgp_type *string_type, - struct mgp_type *bool_type, struct mgp_type *int_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_read_procedure(module, "validate_model", validate_model_cb, &proc)) || - proc == NULL) { +static bool register_prepare(struct mgp_module *module, struct mgp_memory *memory, + struct mgp_type *string_type, struct mgp_type *bool_type, + struct mgp_type *int_type) { + struct mgp_proc *procedure = NULL; + if (!mg_ok( + mgp_module_add_write_procedure(module, "prepare_model", prepare_model_cb, &procedure)) || + procedure == NULL) { return false; } - return add_required(proc, "model", string_type) && add_result(proc, "ok", bool_type) && - add_result(proc, "code", string_type) && add_result(proc, "message", string_type) && - add_result(proc, "states", int_type) && add_result(proc, "letters", int_type) && - add_result(proc, "transitions", int_type); + return add_required(procedure, "model", string_type) && + add_optional_bool(procedure, "materialize_pair_edges", false, memory, bool_type) && + add_result(procedure, "status", string_type) && + add_result(procedure, "generation", int_type) && + add_result(procedure, "states", int_type) && add_result(procedure, "actions", int_type) && + add_result(procedure, "outputs", int_type) && + add_result(procedure, "transitions", int_type) && + add_result(procedure, "pairs", int_type) && + add_result(procedure, "pair_edges", int_type) && + add_result(procedure, "mergeable_pairs", int_type) && + add_result(procedure, "resolvable_pairs", int_type) && + add_result(procedure, "materialized_pair_edges", bool_type); } -static bool register_build_model(struct mgp_module *module, struct mgp_memory *memory, - struct mgp_type *string_type, struct mgp_type *bool_type, - struct mgp_type *int_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_read_procedure(module, "build_model", build_model_cb, &proc)) || - proc == NULL) { - return false; - } - return add_required(proc, "model", string_type) && - add_optional_bool(proc, "complete_with_sink", false, memory, bool_type) && - add_result(proc, "status", string_type) && add_result(proc, "generation", int_type) && - add_result(proc, "states", int_type) && add_result(proc, "letters", int_type) && - add_result(proc, "transitions", int_type); +static bool add_plan_common_results(struct mgp_proc *procedure, struct mgp_type *string_type, + struct mgp_type *int_type, struct mgp_type *list_type) { + return add_result(procedure, "status", string_type) && + add_result(procedure, "outcome", string_type) && + add_result(procedure, "reason", string_type) && + add_result(procedure, "method", string_type) && add_result(procedure, "word", list_type) && + add_result(procedure, "length", int_type) && + add_result(procedure, "expansions", int_type) && + add_result(procedure, "generation", int_type) && + add_result(procedure, "planning_time_us", int_type); } -static bool register_build_pair_oracle(struct mgp_module *module, struct mgp_memory *memory, - struct mgp_type *string_type, struct mgp_type *bool_type, - struct mgp_type *int_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_write_procedure(module, "build_pair_oracle", build_pair_oracle_cb, - &proc)) || - proc == NULL) { +static bool register_plan_sync(struct mgp_module *module, struct mgp_type *string_type, + struct mgp_type *int_type, struct mgp_type *list_type) { + struct mgp_proc *procedure = NULL; + if (!mg_ok(mgp_module_add_read_procedure(module, "plan_sync", plan_sync_cb, &procedure)) || + procedure == NULL) { return false; } - return add_required(proc, "model", string_type) && - add_optional_bool(proc, "materialize", true, memory, bool_type) && - add_result(proc, "status", string_type) && add_result(proc, "pairs", int_type) && - add_result(proc, "pair_edges", int_type) && - add_result(proc, "mergeable_pairs", int_type) && - add_result(proc, "materialized", bool_type) && add_result(proc, "generation", int_type); + return add_required(procedure, "model", string_type) && + add_required(procedure, "hypotheses", list_type) && + add_required(procedure, "budget", int_type) && + add_plan_common_results(procedure, string_type, int_type, list_type) && + add_result(procedure, "final_state_key", string_type) && + add_result(procedure, "final_support_size", int_type); } -static bool register_word_for_set(struct mgp_module *module, struct mgp_memory *memory, - struct mgp_type *string_type, struct mgp_type *int_type, - struct mgp_type *list_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_read_procedure(module, "word_for_set", word_for_set_cb, &proc)) || - proc == NULL) { - return false; - } - return add_required(proc, "model", string_type) && add_required(proc, "state_keys", list_type) && - add_optional_empty_list(proc, "target_keys", memory, list_type) && - add_optional_string(proc, "mode", "SYNC", memory, string_type) && - add_optional_int(proc, "budget", 0, memory, int_type) && - add_result(proc, "status", string_type) && add_result(proc, "word", list_type) && - add_result(proc, "length", int_type) && add_result(proc, "final_state_key", string_type) && - add_result(proc, "generation", int_type); -} - -static bool register_word_to_target(struct mgp_module *module, struct mgp_memory *memory, - struct mgp_type *string_type, struct mgp_type *int_type, - struct mgp_type *list_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_read_procedure(module, "word_to_target", word_to_target_cb, &proc)) || - proc == NULL) { +static bool register_plan_disambiguate(struct mgp_module *module, struct mgp_type *string_type, + struct mgp_type *bool_type, struct mgp_type *int_type, + struct mgp_type *list_type) { + struct mgp_proc *procedure = NULL; + if (!mg_ok(mgp_module_add_read_procedure(module, "plan_disambiguate", plan_disambiguate_cb, + &procedure)) || + procedure == NULL) { return false; } - return add_required(proc, "model", string_type) && add_required(proc, "state_keys", list_type) && - add_required(proc, "target_keys", list_type) && - add_optional_string(proc, "mode", "REACH_AND_SYNC", memory, string_type) && - add_optional_int(proc, "budget", 0, memory, int_type) && - add_result(proc, "status", string_type) && add_result(proc, "word", list_type) && - add_result(proc, "length", int_type) && add_result(proc, "final_state_key", string_type) && - add_result(proc, "generation", int_type); + return add_required(procedure, "model", string_type) && + add_required(procedure, "hypotheses", list_type) && + add_required(procedure, "bound", int_type) && + add_required(procedure, "budget", int_type) && + add_plan_common_results(procedure, string_type, int_type, list_type) && + add_result(procedure, "best_support_size", int_type) && + add_result(procedure, "worst_support_size", int_type) && + add_result(procedure, "branch_count", int_type) && + add_result(procedure, "homing", bool_type); } static bool register_explain(struct mgp_module *module, struct mgp_type *string_type, struct mgp_type *int_type, struct mgp_type *list_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_read_procedure(module, "explain", explain_cb, &proc)) || proc == NULL) { + struct mgp_proc *procedure = NULL; + if (!mg_ok(mgp_module_add_read_procedure(module, "explain_plan", explain_plan_cb, &procedure)) || + procedure == NULL) { return false; } - return add_required(proc, "model", string_type) && add_required(proc, "state_keys", list_type) && - add_required(proc, "word", list_type) && add_result(proc, "step", int_type) && - add_result(proc, "letter", string_type) && - add_result(proc, "active_state_keys", list_type); + return add_required(procedure, "model", string_type) && + add_required(procedure, "generation", int_type) && + add_required(procedure, "hypotheses", list_type) && + add_required(procedure, "word", list_type) && add_result(procedure, "step", int_type) && + add_result(procedure, "action", string_type) && + add_result(procedure, "predicted_hypotheses", list_type) && + add_result(procedure, "output_trace", list_type) && + add_result(procedure, "branch_hypotheses", list_type) && + add_result(procedure, "generation", int_type); } -static bool register_expand_cache(struct mgp_module *module, struct mgp_memory *memory, - struct mgp_type *string_type, struct mgp_type *int_type, - struct mgp_type *list_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_write_procedure(module, "expand_cache", expand_cache_cb, &proc)) || - proc == NULL) { +static bool register_validate(struct mgp_module *module, struct mgp_memory *memory, + struct mgp_type *string_type, struct mgp_type *bool_type, + struct mgp_type *int_type, struct mgp_type *list_type) { + struct mgp_proc *procedure = NULL; + if (!mg_ok(mgp_module_add_read_procedure(module, "validate_update", validate_update_cb, + &procedure)) || + procedure == NULL) { return false; } - return add_required(proc, "model", string_type) && add_required(proc, "target_keys", list_type) && - add_optional_string(proc, "mode", "REACH_AND_SYNC", memory, string_type) && - add_optional_int(proc, "budget", 0, memory, int_type) && - add_result(proc, "status", string_type) && add_result(proc, "expanded", int_type) && - add_result(proc, "cache_size", int_type); + return add_required(procedure, "model", string_type) && + add_required(procedure, "generation", int_type) && + add_required(procedure, "hypotheses", list_type) && + add_required(procedure, "word", list_type) && + add_required(procedure, "completed_steps", int_type) && + add_required(procedure, "reported_hypotheses", list_type) && + add_optional_bool(procedure, "localizer_available", true, memory, bool_type) && + add_result(procedure, "status", string_type) && + add_result(procedure, "decision", string_type) && + add_result(procedure, "reason", string_type) && + add_result(procedure, "expected_hypotheses", list_type) && + add_result(procedure, "unexpected_hypotheses", list_type) && + add_result(procedure, "generation", int_type); } -static bool register_dirty_write(struct mgp_module *module, const char *name, mgp_proc_cb callback, - struct mgp_type *string_type, struct mgp_type *int_type) { - struct mgp_proc *proc = NULL; - if (!mg_ok(mgp_module_add_write_procedure(module, name, callback, &proc)) || proc == NULL) { +static bool register_mark_dirty(struct mgp_module *module, struct mgp_type *string_type, + struct mgp_type *int_type) { + struct mgp_proc *procedure = NULL; + if (!mg_ok(mgp_module_add_write_procedure(module, "mark_dirty", mark_dirty_cb, &procedure)) || + procedure == NULL) { return false; } - return add_required(proc, "model", string_type) && add_result(proc, "status", string_type) && - add_result(proc, "generation", int_type); + return add_required(procedure, "model", string_type) && + add_result(procedure, "status", string_type) && + add_result(procedure, "generation", int_type); } int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) { struct mgp_type *string_type = NULL; struct mgp_type *bool_type = NULL; struct mgp_type *int_type = NULL; - struct mgp_type *any_type = NULL; - struct mgp_type *nullable_any_type = NULL; struct mgp_type *list_type = NULL; if (!mg_ok(mgp_type_string(&string_type)) || !mg_ok(mgp_type_bool(&bool_type)) || - !mg_ok(mgp_type_int(&int_type)) || !mg_ok(mgp_type_any(&any_type)) || - !mg_ok(mgp_type_nullable(any_type, &nullable_any_type)) || - !mg_ok(mgp_type_list(nullable_any_type, &list_type))) { + !mg_ok(mgp_type_int(&int_type)) || !mg_ok(mgp_type_list(string_type, &list_type))) { return 1; } - - if (!register_validate(module, string_type, bool_type, int_type) || - !register_build_model(module, memory, string_type, bool_type, int_type) || - !register_build_pair_oracle(module, memory, string_type, bool_type, int_type) || - !register_word_for_set(module, memory, string_type, int_type, list_type) || - !register_word_to_target(module, memory, string_type, int_type, list_type) || - !register_expand_cache(module, memory, string_type, int_type, list_type) || + if (!register_prepare(module, memory, string_type, bool_type, int_type) || + !register_plan_sync(module, string_type, int_type, list_type) || + !register_plan_disambiguate(module, string_type, bool_type, int_type, list_type) || !register_explain(module, string_type, int_type, list_type) || - !register_dirty_write(module, "mark_dirty", mark_dirty_cb, string_type, int_type) || - !register_dirty_write(module, "on_transition_delta", on_transition_delta_cb, string_type, - int_type)) { + !register_validate(module, memory, string_type, bool_type, int_type, list_type) || + !register_mark_dirty(module, string_type, int_type)) { return 1; } return 0; diff --git a/views/sync_automata.gss b/views/sync_automata.gss index 261c5d9..f68f5a8 100644 --- a/views/sync_automata.gss +++ b/views/sync_automata.gss @@ -10,10 +10,16 @@ label: Format("{}", Property(node, "state_key")); } -@NodeStyle HasLabel(node, "SyncLetter") { +@NodeStyle HasLabel(node, "SyncAction") { color: #c67c2f; size: 12; - label: Format("{}", Property(node, "letter")); + label: Format("{}", Property(node, "action_key")); +} + +@NodeStyle HasLabel(node, "SyncOutput") { + color: #b84a62; + size: 12; + label: Format("{}", Property(node, "output_key")); } @NodeStyle HasLabel(node, "SyncPair") { @@ -22,20 +28,20 @@ label: Format("{}|{}", Property(node, "first_key"), Property(node, "second_key")); } -@NodeStyle HasLabel(node, "SyncSubset") { - color: #8b6f2f; - size: 10; - label: Format("{}", Property(node, "subset_key")); -} - @EdgeStyle Type(edge) == "SYNC_TRANS" { color: #444444; width: 2; - label: Format("{}", Property(edge, "letter")); + label: Format("{}", Property(edge, "action_key")); +} + +@EdgeStyle Type(edge) == "SYNC_OBS" { + color: #b84a62; + width: 2; + label: Format("{}", Property(edge, "action_key")); } @EdgeStyle Type(edge) == "PAIR_NEXT" || Type(edge) == "PAIR_PRE" { color: #777777; width: 1; - label: Format("{}", Property(edge, "letter")); + label: Format("{}", Property(edge, "action")); } From 16c20194bd1441ba7b99edc051e8091efe87c796 Mon Sep 17 00:00:00 2001 From: gaperez64 Date: Mon, 17 Aug 2026 21:36:30 +0200 Subject: [PATCH 6/7] Gate full production coverage in CI --- .github/workflows/ci.yml | 44 +++++- scripts/coverage.sh | 46 +++++- scripts/memgraph_integration.sh | 245 ++++++++++++++++++++++++++++++++ scripts/memgraph_local_smoke.sh | 134 +++++++++-------- scripts/memgraph_smoke.sh | 88 +++++------- 5 files changed, 439 insertions(+), 118 deletions(-) mode change 100644 => 100755 scripts/coverage.sh create mode 100755 scripts/memgraph_integration.sh mode change 100644 => 100755 scripts/memgraph_local_smoke.sh mode change 100644 => 100755 scripts/memgraph_smoke.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d1e2e7..a7b2287 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - name: Install toolchain run: | sudo apt-get update - sudo apt-get install -y clang clang-format clang-tidy gcovr llvm ninja-build python3-venv valgrind + sudo apt-get install -y clang clang-format clang-tidy ninja-build python3-venv valgrind python3 -m venv "$RUNNER_TEMP/meson-venv" "$RUNNER_TEMP/meson-venv/bin/pip" install 'meson>=1.6' echo "$RUNNER_TEMP/meson-venv/bin" >> "$GITHUB_PATH" @@ -32,13 +32,15 @@ jobs: run: sh scripts/check-format.sh - name: Clang tidy run: sh scripts/run-clang-tidy.sh build-tidy - - name: Coverage - run: sh scripts/coverage.sh build-coverage - name: CLI smoke - run: ./build/sync-kgraph-cli --example office + run: ./build/sync-kgraph-cli --example warehouse memgraph-module-compile: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + memgraph-version: ['3.1.1', '3.7.0', '3.11.0'] steps: - uses: actions/checkout@v4 - name: Install toolchain @@ -55,7 +57,7 @@ jobs: - name: Fetch Memgraph C API header run: | mkdir -p third_party/memgraph/include - curl -L https://raw.githubusercontent.com/memgraph/memgraph/v3.11.0/include/mg_procedure.h \ + curl -L https://raw.githubusercontent.com/memgraph/memgraph/v${{ matrix.memgraph-version }}/include/mg_procedure.h \ -o third_party/memgraph/include/mg_procedure.h - name: Build module run: | @@ -64,6 +66,38 @@ jobs: -Dmemgraph_include_dir="$PWD/third_party/memgraph/include" ninja -C build-memgraph - name: Clang tidy module + if: matrix.memgraph-version == '3.11.0' run: sh scripts/run-clang-tidy.sh build-memgraph-tidy "$PWD/third_party/memgraph/include" - name: Memgraph smoke + if: matrix.memgraph-version == '3.11.0' run: sh scripts/memgraph_smoke.sh build-memgraph + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + run: | + sudo apt-get update + sudo apt-get install -y clang curl gcovr llvm ninja-build python3-venv + python3 -m venv "$RUNNER_TEMP/meson-venv" + "$RUNNER_TEMP/meson-venv/bin/pip" install 'meson>=1.6' + echo "$RUNNER_TEMP/meson-venv/bin" >> "$GITHUB_PATH" + - name: Fetch Memgraph C API header + run: | + mkdir -p third_party/memgraph/include + curl -L https://raw.githubusercontent.com/memgraph/memgraph/v3.11.0/include/mg_procedure.h \ + -o third_party/memgraph/include/mg_procedure.h + - name: Full production coverage + env: + SYNC_KGRAPH_COVERAGE_RUNNER: docker + run: sh scripts/coverage.sh build-coverage "$PWD/third_party/memgraph/include" + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: | + build-coverage/coverage.xml + build-coverage/coverage.html + build-coverage/coverage.*.html diff --git a/scripts/coverage.sh b/scripts/coverage.sh old mode 100644 new mode 100755 index 9282273..4488fac --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -2,15 +2,57 @@ set -eu builddir="${1:-build-coverage}" +memgraph_include_dir="${2:-}" -CC="${CC:-clang}" meson setup "$builddir" --wipe -Db_coverage=true -Dmemgraph=disabled +if [ -z "$memgraph_include_dir" ] && [ -f /usr/include/memgraph/mg_procedure.h ]; then + memgraph_include_dir=/usr/include/memgraph +fi +if [ -z "$memgraph_include_dir" ] || + [ ! -f "$memgraph_include_dir/mg_procedure.h" ]; then + echo "coverage requires a directory containing mg_procedure.h" >&2 + exit 2 +fi + +CC="${CC:-clang}" meson setup "$builddir" --wipe \ + -Db_coverage=true \ + -Dmemgraph=enabled \ + -Dmemgraph_include_dir="$memgraph_include_dir" +meson compile -C "$builddir" meson test -C "$builddir" --print-errorlogs +case "$builddir" in +/*) cli="$builddir/sync-kgraph-cli" ;; +*) cli="./$builddir/sync-kgraph-cli" ;; +esac +"$cli" --example warehouse >/dev/null + +case "${SYNC_KGRAPH_COVERAGE_RUNNER:-auto}" in +auto) + if [ -x /usr/lib/memgraph/memgraph ] && command -v mgconsole >/dev/null 2>&1; then + sh scripts/memgraph_local_smoke.sh "$builddir" + else + SYNC_KGRAPH_COVERAGE=1 sh scripts/memgraph_smoke.sh "$builddir" + fi + ;; +local) + sh scripts/memgraph_local_smoke.sh "$builddir" + ;; +docker) + SYNC_KGRAPH_COVERAGE=1 sh scripts/memgraph_smoke.sh "$builddir" + ;; +*) + echo "SYNC_KGRAPH_COVERAGE_RUNNER must be auto, local, or docker" >&2 + exit 2 + ;; +esac + gcovr \ --root . \ --object-directory "$builddir" \ --gcov-executable "llvm-cov gcov" \ - --filter 'src/(oracle|planner|sync)\.c' \ + --filter 'src/.*\.c' \ --exclude 'tests/.*' \ --fail-under-line 75 \ + --xml "$builddir/coverage.xml" \ + --html-details "$builddir/coverage.html" \ --print-summary \ "$builddir" diff --git a/scripts/memgraph_integration.sh b/scripts/memgraph_integration.sh new file mode 100755 index 0000000..a4ed096 --- /dev/null +++ b/scripts/memgraph_integration.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env sh +set -eu + +mode="${1:-local}" +container="${2:-}" +host="${MEMGRAPH_HOST:-127.0.0.1}" +port="${MEMGRAPH_PORT:-7687}" +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) + +run_query() { + if [ "$mode" = "docker" ]; then + printf '%s\n' "$1" | docker exec -i "$container" mgconsole \ + --host=127.0.0.1 \ + --port=7687 \ + --output_format=csv \ + --no_history + else + printf '%s\n' "$1" | mgconsole \ + --host="$host" \ + --port="$port" \ + --output_format=csv \ + --no_history + fi +} + +run_file() { + if [ "$mode" = "docker" ]; then + docker exec -i "$container" mgconsole \ + --host=127.0.0.1 \ + --port=7687 \ + --output_format=csv \ + --no_history <"$1" + else + mgconsole \ + --host="$host" \ + --port="$port" \ + --output_format=csv \ + --no_history <"$1" + fi +} + +assert_pass() { + name="$1" + query="$2" + output=$(run_query "$query") || { + printf '%s\n' "integration check failed to execute: $name" >&2 + exit 1 + } + if ! printf '%s\n' "$output" | grep -q 'PASS'; then + printf '%s\n' "integration check failed: $name" "$output" >&2 + exit 1 + fi +} + +expect_failure() { + name="$1" + query="$2" + if run_query "$query" >/dev/null 2>&1; then + printf '%s\n' "integration query unexpectedly succeeded: $name" >&2 + exit 1 + fi +} + +case "$mode" in +local) + ;; +docker) + if [ -z "$container" ]; then + echo "docker mode requires a container name" >&2 + exit 2 + fi + ;; +*) + echo "usage: $0 [local | docker ]" >&2 + exit 2 + ;; +esac + +run_query 'CALL mg.load("sync");' >/dev/null +run_file "$root/examples/warehouse/00_reset_and_load.cypher" >/dev/null + +assert_pass "registered procedures" ' +CALL mg.procedures() YIELD name +WITH name +WHERE name STARTS WITH "sync." +WITH count(name) AS procedures +RETURN CASE WHEN procedures = 6 THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "prepare model" ' +CALL sync.prepare_model("warehouse", true) +YIELD status, generation, states, actions, outputs, transitions, pairs, + pair_edges, mergeable_pairs, resolvable_pairs, materialized_pair_edges +RETURN CASE WHEN status = "OK" AND generation = 1 AND states = 5 + AND actions = 4 AND outputs = 4 AND transitions = 20 + AND pairs = 15 AND pair_edges = 60 + AND mergeable_pairs = 15 AND resolvable_pairs = 15 + AND materialized_pair_edges + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "materialized pair records" ' +MATCH (p:SyncPair {model: "warehouse", generation: 1}) +WITH count(p) AS pairs +OPTIONAL MATCH (:SyncPair {model: "warehouse", generation: 1}) + -[n:PAIR_NEXT {model: "warehouse", generation: 1}]-> + (:SyncPair {model: "warehouse", generation: 1}) +WITH pairs, count(n) AS next_edges +OPTIONAL MATCH (:SyncPair {model: "warehouse", generation: 1}) + -[p:PAIR_PRE {model: "warehouse", generation: 1}]-> + (:SyncPair {model: "warehouse", generation: 1}) +WITH pairs, next_edges, count(p) AS pre_edges +RETURN CASE WHEN pairs = 15 AND next_edges = 60 AND pre_edges = 60 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "synchronization plan" ' +CALL sync.plan_sync( + "warehouse", ["west_bay:east", "east_bay:west"], 64) +YIELD status, outcome, method, word, length, final_state_key, + final_support_size, generation +RETURN CASE WHEN status = "OK" AND outcome = "PLAN" + AND method = "PAIR_MERGE" + AND word = ["to_corridor", "go_west"] AND length = 2 + AND final_state_key = "dock:north" + AND final_support_size = 1 AND generation = 1 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "disambiguation plan" ' +CALL sync.plan_disambiguate( + "warehouse", ["west_bay:east", "east_bay:west"], 1, 64) +YIELD status, outcome, method, word, length, best_support_size, + worst_support_size, branch_count, homing, generation +RETURN CASE WHEN status = "OK" AND outcome = "PLAN" + AND method = "PAIR_RESOLUTION" AND word = ["to_corridor"] + AND length = 1 AND best_support_size = 1 + AND worst_support_size = 1 AND branch_count = 2 + AND homing AND generation = 1 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "plan explanation" ' +CALL sync.explain_plan( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"]) +YIELD step, branch_hypotheses +WITH count(*) AS rows, + sum(CASE WHEN step = 0 THEN 1 ELSE 0 END) AS initial_rows, + sum(CASE WHEN step = 1 THEN 1 ELSE 0 END) AS first_step_rows, + sum(CASE WHEN step = 2 AND branch_hypotheses = ["dock:north"] + THEN 1 ELSE 0 END) AS terminal_rows +RETURN CASE WHEN rows = 5 AND initial_rows = 1 AND first_step_rows = 2 + AND terminal_rows = 2 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "monitor continue" ' +CALL sync.validate_update( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], 1, + ["corridor_w:east", "corridor_e:west"], true) +YIELD status, decision, expected_hypotheses, unexpected_hypotheses, generation +RETURN CASE WHEN status = "OK" AND decision = "CONTINUE" + AND size(expected_hypotheses) = 2 + AND size(unexpected_hypotheses) = 0 AND generation = 1 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "monitor replan" ' +CALL sync.validate_update( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], 1, + ["corridor_w:east"], true) +YIELD status, decision, expected_hypotheses, unexpected_hypotheses +RETURN CASE WHEN status = "OK" AND decision = "REPLAN" + AND size(expected_hypotheses) = 2 + AND size(unexpected_hypotheses) = 0 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "monitor model violation" ' +CALL sync.validate_update( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], 1, + ["dock:north"], true) +YIELD status, decision, unexpected_hypotheses +RETURN CASE WHEN status = "OK" AND decision = "MODEL_VIOLATION" + AND unexpected_hypotheses = ["dock:north"] + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "monitor wait" ' +CALL sync.validate_update( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], 1, [], false) +YIELD status, decision, expected_hypotheses, unexpected_hypotheses +RETURN CASE WHEN status = "OK" AND decision = "WAIT" + AND size(expected_hypotheses) = 2 + AND size(unexpected_hypotheses) = 0 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "monitor stale generation" ' +CALL sync.validate_update( + "warehouse", 0, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], 1, [], false) +YIELD status, decision, expected_hypotheses, unexpected_hypotheses, generation +RETURN CASE WHEN status = "OK" AND decision = "STALE_GENERATION" + AND size(expected_hypotheses) = 0 + AND size(unexpected_hypotheses) = 0 AND generation = 1 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "mark dirty" ' +CALL sync.mark_dirty("warehouse") YIELD status, generation +RETURN CASE WHEN status = "DIRTY" AND generation = 2 + THEN "PASS" ELSE "FAIL" END AS result;' + +expect_failure "planning rejects dirty model" ' +CALL sync.plan_sync( + "warehouse", ["west_bay:east", "east_bay:west"], 64) +YIELD status RETURN status;' + +assert_pass "old plan is stale after model change" ' +CALL sync.validate_update( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], 1, [], false) +YIELD status, decision, generation +RETURN CASE WHEN status = "OK" AND decision = "STALE_GENERATION" + AND generation = 2 + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "reprepare without pair edges" ' +CALL sync.prepare_model("warehouse", false) +YIELD status, generation, pairs, pair_edges, materialized_pair_edges +RETURN CASE WHEN status = "OK" AND generation = 2 AND pairs = 15 + AND pair_edges = 60 AND NOT materialized_pair_edges + THEN "PASS" ELSE "FAIL" END AS result;' + +assert_pass "optional pair edges removed" ' +MATCH (p:SyncPair {model: "warehouse", generation: 2}) +WITH count(p) AS pairs +OPTIONAL MATCH (:SyncPair {model: "warehouse"})-[r:PAIR_NEXT|PAIR_PRE]->() +WITH pairs, count(r) AS edges +RETURN CASE WHEN pairs = 15 AND edges = 0 + THEN "PASS" ELSE "FAIL" END AS result;' + +echo "Memgraph integration test passed" diff --git a/scripts/memgraph_local_smoke.sh b/scripts/memgraph_local_smoke.sh old mode 100644 new mode 100755 index 7e3cf92..5cb9564 --- a/scripts/memgraph_local_smoke.sh +++ b/scripts/memgraph_local_smoke.sh @@ -1,69 +1,91 @@ #!/usr/bin/env sh set -eu -host="${MEMGRAPH_HOST:-127.0.0.1}" -port="${MEMGRAPH_PORT:-7687}" -model="${SYNC_KGRAPH_SMOKE_MODEL:-sync_kgraph_smoke}" +builddir="${1:-build-memgraph-local}" +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +case "$builddir" in +/*) module="$builddir/sync.so" ;; +*) module="$root/$builddir/sync.so" ;; +esac +memgraph="${MEMGRAPH_BINARY:-/usr/lib/memgraph/memgraph}" +port="${MEMGRAPH_TEST_PORT:-$((17687 + ($$ % 10000)))}" +tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/sync-kgraph-memgraph.XXXXXX") +pid="" -run_query() { - printf '%s\n' "$1" | mgconsole \ - --host="$host" \ - --port="$port" \ - --output_format=csv \ - --no_history +cleanup() { + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi + rm -rf "$tmpdir" } +trap cleanup EXIT HUP INT TERM -run_query 'CALL mg.load("sync");' >/dev/null - -run_query "MATCH (n) -WHERE (n:SyncModel OR n:SyncState OR n:SyncLetter OR n:SyncPair OR n:SyncSubset) - AND n.model = \"$model\" -DETACH DELETE n;" >/dev/null - -run_query "CREATE (:SyncModel {model: \"$model\", generation: 0, dirty: false}); -CREATE (:SyncState {model: \"$model\", state_key: \"A\", state_id: 0}); -CREATE (:SyncState {model: \"$model\", state_key: \"B\", state_id: 1}); -CREATE (:SyncState {model: \"$model\", state_key: \"C\", state_id: 2}); -CREATE (:SyncLetter {model: \"$model\", letter: \"north\", letter_id: 0}); -CREATE (:SyncLetter {model: \"$model\", letter: \"east\", letter_id: 1}); -MATCH (a:SyncState {model: \"$model\", state_key: \"A\"}) -MATCH (b:SyncState {model: \"$model\", state_key: \"B\"}) -MATCH (c:SyncState {model: \"$model\", state_key: \"C\"}) -CREATE (a)-[:SYNC_TRANS {model: \"$model\", letter: \"north\"}]->(b) -CREATE (b)-[:SYNC_TRANS {model: \"$model\", letter: \"north\"}]->(c) -CREATE (c)-[:SYNC_TRANS {model: \"$model\", letter: \"north\"}]->(c) -CREATE (a)-[:SYNC_TRANS {model: \"$model\", letter: \"east\"}]->(a) -CREATE (b)-[:SYNC_TRANS {model: \"$model\", letter: \"east\"}]->(b) -CREATE (c)-[:SYNC_TRANS {model: \"$model\", letter: \"east\"}]->(c);" >/dev/null - -validate="$(run_query "CALL sync.validate_model(\"$model\") YIELD ok, code RETURN ok, code;")" -printf '%s\n' "$validate" | grep -q "true" -printf '%s\n' "$validate" | grep -q "OK" - -oracle="$(run_query "CALL sync.build_pair_oracle(\"$model\") YIELD status, pairs, pair_edges, mergeable_pairs, materialized RETURN status, pairs, pair_edges, mergeable_pairs, materialized;")" -printf '%s\n' "$oracle" | grep -q "OK" -printf '%s\n' "$oracle" | grep -q "true" +dump_logs() { + if [ -f "$tmpdir/stderr.log" ]; then + cat "$tmpdir/stderr.log" >&2 + fi + if [ -f "$tmpdir/memgraph.log" ]; then + cat "$tmpdir/memgraph.log" >&2 + fi +} -pairs="$(run_query "MATCH (p:SyncPair {model: \"$model\"}) RETURN count(p) AS pairs;")" -printf '%s\n' "$pairs" | grep -q "6" +if [ ! -f "$module" ]; then + echo "missing Memgraph module: $module" >&2 + exit 2 +fi +if [ ! -x "$memgraph" ]; then + echo "missing Memgraph executable: $memgraph" >&2 + exit 2 +fi +if ! command -v mgconsole >/dev/null 2>&1; then + echo "mgconsole is required for the local integration test" >&2 + exit 2 +fi -pair_edges="$(run_query "MATCH (:SyncPair {model: \"$model\"})-[r:PAIR_NEXT {model: \"$model\"}]->(:SyncPair {model: \"$model\"}) RETURN count(r) AS pair_next;")" -printf '%s\n' "$pair_edges" | grep -q "12" +mkdir -p "$tmpdir/data" "$tmpdir/modules" "$tmpdir/query-logs" +cp "$module" "$tmpdir/modules/sync.so" -word="$(run_query "CALL sync.word_to_target(\"$model\", [\"A\", \"B\"], [\"C\"], \"REACH_AND_SYNC\", 64) YIELD status, word, length, final_state_key RETURN status, word, length, final_state_key;")" -printf '%s\n' "$word" | grep -q "PAIR_GREEDY_TARGETED" -printf '%s\n' "$word" | grep -q "north" -printf '%s\n' "$word" | grep -q "C" +"$memgraph" \ + --bolt-address=127.0.0.1 \ + --bolt-port="$port" \ + --data-directory="$tmpdir/data" \ + --data-recovery-on-startup=false \ + --log-file="$tmpdir/memgraph.log" \ + --metrics-address=127.0.0.1 \ + --metrics-port="$((port + 1))" \ + --monitoring-address=127.0.0.1 \ + --monitoring-port="$((port + 2))" \ + --query-log-directory="$tmpdir/query-logs" \ + --query-modules-directory="$tmpdir/modules" \ + --storage-snapshot-interval-sec=0 \ + --storage-snapshot-on-exit=false \ + --storage-wal-enabled=false \ + --telemetry-enabled=false >"$tmpdir/stderr.log" 2>&1 & +pid=$! -cache="$(run_query "CALL sync.expand_cache(\"$model\", [\"C\"], \"REACH_AND_SYNC\", 64) YIELD status, expanded, cache_size RETURN status, expanded, cache_size;")" -printf '%s\n' "$cache" | grep -q "OK" +ready=0 +for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do + if ! kill -0 "$pid" 2>/dev/null; then + dump_logs + echo "isolated Memgraph process exited before becoming ready" >&2 + exit 1 + fi + if printf 'RETURN 1;\n' | mgconsole \ + --host=127.0.0.1 --port="$port" --no_history >/dev/null 2>&1; then + ready=1 + break + fi + sleep 1 +done -subsets="$(run_query "MATCH (s:SyncSubset {model: \"$model\", mode: \"REACH_AND_SYNC\", target_key: \"C\"}) RETURN count(s) AS subsets;")" -printf '%s\n' "$subsets" | grep -q "3" +if [ "$ready" -ne 1 ]; then + dump_logs + echo "isolated Memgraph process did not become ready" >&2 + exit 1 +fi -run_query "MATCH (n) -WHERE (n:SyncModel OR n:SyncState OR n:SyncLetter OR n:SyncPair OR n:SyncSubset) - AND n.model = \"$model\" -DETACH DELETE n;" >/dev/null +MEMGRAPH_HOST=127.0.0.1 MEMGRAPH_PORT="$port" \ + sh "$root/scripts/memgraph_integration.sh" local -echo "Local Memgraph smoke test passed" +echo "Local smoke used isolated temporary data and left the installed database untouched" diff --git a/scripts/memgraph_smoke.sh b/scripts/memgraph_smoke.sh old mode 100644 new mode 100755 index c4a2d1e..57c9b8d --- a/scripts/memgraph_smoke.sh +++ b/scripts/memgraph_smoke.sh @@ -2,9 +2,15 @@ set -eu builddir="${1:-build-memgraph}" -module="$PWD/$builddir/sync.so" +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +case "$builddir" in +/*) module="$builddir/sync.so" ;; +*) module="$root/$builddir/sync.so" ;; +esac image="${MEMGRAPH_IMAGE:-memgraph/memgraph:3.11.0}" container="sync-kgraph-smoke-$$" +absolute_builddir=$(dirname -- "$module") +absolute_builddir=$(CDPATH= cd -- "$absolute_builddir" && pwd) if [ ! -f "$module" ]; then echo "missing Memgraph module: $module" >&2 @@ -12,36 +18,36 @@ if [ ! -f "$module" ]; then fi cleanup() { + docker stop --time 30 "$container" >/dev/null 2>&1 || true docker rm -f "$container" >/dev/null 2>&1 || true } -trap cleanup EXIT - -docker run \ - -d \ - --name "$container" \ - -v "$module:/usr/lib/memgraph/query_modules/sync.so:ro" \ - "$image" \ - --also-log-to-stderr >/dev/null - -run_query() { - printf '%s\n' "$1" | docker exec -i "$container" mgconsole \ - --host=127.0.0.1 \ - --port=7687 \ - --output_format=csv \ - --no_history -} - -run_file() { - docker exec -i "$container" mgconsole \ - --host=127.0.0.1 \ - --port=7687 \ - --output_format=csv \ - --no_history <"$1" -} +trap cleanup EXIT HUP INT TERM + +if [ "${SYNC_KGRAPH_COVERAGE:-0}" = "1" ]; then + strip=$(printf '%s\n' "$absolute_builddir" | awk -F/ '{print NF - 1}') + chmod -R a+rwX "$absolute_builddir" + docker run \ + -d \ + --name "$container" \ + -e GCOV_PREFIX=/coverage \ + -e GCOV_PREFIX_STRIP="$strip" \ + -v "$absolute_builddir:/coverage" \ + -v "$module:/usr/lib/memgraph/query_modules/sync.so:ro" \ + "$image" \ + --also-log-to-stderr >/dev/null +else + docker run \ + -d \ + --name "$container" \ + -v "$module:/usr/lib/memgraph/query_modules/sync.so:ro" \ + "$image" \ + --also-log-to-stderr >/dev/null +fi ready=0 for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do - if run_query "RETURN 1;" >/dev/null 2>&1; then + if printf 'RETURN 1;\n' | docker exec -i "$container" mgconsole \ + --host=127.0.0.1 --port=7687 --no_history >/dev/null 2>&1; then ready=1 break fi @@ -54,32 +60,4 @@ if [ "$ready" -ne 1 ]; then exit 1 fi -run_query 'CALL mg.load("sync");' >/dev/null -run_file examples/office/00_reset_and_load.cypher >/dev/null - -validate="$(run_query 'CALL sync.validate_model("office") YIELD ok, code RETURN ok, code;')" -printf '%s\n' "$validate" | grep -q "true" -printf '%s\n' "$validate" | grep -q "OK" - -oracle="$(run_query 'CALL sync.build_pair_oracle("office") YIELD status, pairs, pair_edges, mergeable_pairs, materialized RETURN status, pairs, pair_edges, mergeable_pairs, materialized;')" -printf '%s\n' "$oracle" | grep -q "OK" -printf '%s\n' "$oracle" | grep -q "true" - -pairs="$(run_query 'MATCH (p:SyncPair {model: "office"}) RETURN count(p) AS pairs;')" -printf '%s\n' "$pairs" | grep -q "6" - -pair_edges="$(run_query 'MATCH (:SyncPair {model: "office"})-[r:PAIR_NEXT {model: "office"}]->(:SyncPair {model: "office"}) RETURN count(r) AS pair_next;')" -printf '%s\n' "$pair_edges" | grep -q "12" - -word="$(run_query 'CALL sync.word_to_target("office", ["A", "B"], ["C"], "REACH_AND_SYNC", 64) YIELD status, word, length, final_state_key RETURN status, word, length, final_state_key;')" -printf '%s\n' "$word" | grep -q "PAIR_GREEDY_TARGETED" -printf '%s\n' "$word" | grep -q "north" -printf '%s\n' "$word" | grep -q "C" - -cache="$(run_query 'CALL sync.expand_cache("office", ["C"], "REACH_AND_SYNC", 64) YIELD status, expanded, cache_size RETURN status, expanded, cache_size;')" -printf '%s\n' "$cache" | grep -q "OK" - -subsets="$(run_query 'MATCH (s:SyncSubset {model: "office", mode: "REACH_AND_SYNC", target_key: "C"}) RETURN count(s) AS subsets;')" -printf '%s\n' "$subsets" | grep -q "3" - -echo "Memgraph smoke test passed" +sh "$root/scripts/memgraph_integration.sh" docker "$container" From 12642eb6f18762e68c69ac65c4f63f7584a14973 Mon Sep 17 00:00:00 2001 From: gaperez64 Date: Mon, 17 Aug 2026 21:36:41 +0200 Subject: [PATCH 7/7] Document installation and warehouse workflow --- README.md | 373 ++++++++++++++++++++++--------------- scripts/package_release.sh | 12 +- 2 files changed, 228 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 4150b58..a33baf4 100644 --- a/README.md +++ b/README.md @@ -1,277 +1,340 @@ # sync-kgraph -`sync-kgraph` implements schema-agnostic synchronizing-word queries over a -deterministic automaton view of a knowledge graph. The core algorithms are C23 -and build with Meson. Query/view glue is Cypher, and the native Memgraph module -is compiled from C when Memgraph's `mg_procedure.h` is available. +`sync-kgraph` implements the synchronize-or-reveal algorithms from the +companion paper as a C23 library and a native Memgraph query module. It treats a +manually mapped knowledge-graph view as a deterministic Mealy automaton and can: -Implemented pieces: +- validate complete transition and observation functions; +- build and persist a generation-scoped pair merge/resolution oracle; +- plan an open-loop word that synchronizes a hypothesis set; +- plan an output-partitioning word that reveals the current hypothesis; +- fall back to exact bounded partition BFS when a pair witness is insufficient; +- explain predicted supports and output branches after every action; and +- monitor localization updates as `CONTINUE`, `REPLAN`, `MODEL_VIOLATION`, + `STALE_GENERATION`, or `WAIT`. -- DFA view validation for completeness and determinism. -- Reverse transition/preimage support. -- Reverse pair graph with BFS pair-compression witnesses, materialized as - `SyncPair`, `PAIR_NEXT`, and `PAIR_PRE`. -- Greedy synchronizing-word construction for hypothesis sets. -- Target validation for `SYNC`, `REACH`, and `REACH_AND_SYNC`. -- Bounded reverse-subset expansion for exact reachability cases, materialized - as `SyncSubset`. -- Step-by-step explanation of active hypotheses after each action. +All algorithm and Memgraph module code is C23. Cypher is used only for schema, +mapping, and example queries. Meson builds the library, CLI, tests, and optional +Memgraph module. -## Build +## Build And Test + +Core build: ```sh CC=clang meson setup build -Dmemgraph=disabled -ninja -C build +meson compile -C build meson test -C build --print-errorlogs -sh scripts/valgrind.sh build-valgrind -./build/sync-kgraph-cli --example office +./build/sync-kgraph-cli --example warehouse ``` -To build the Memgraph module, point Meson at the Memgraph C API header: +Build the native module against the header installed with the target Memgraph +server. This is preferred over using a header from a different release: ```sh CC=clang meson setup build-memgraph \ -Dmemgraph=enabled \ -Dmemgraph_include_dir=/usr/include/memgraph -ninja -C build-memgraph -``` - -For a locally installed Memgraph, prefer the installed header so the module is -compiled against the server ABI: - -```sh -CC=clang meson setup build-local-memgraph \ - -Dmemgraph=enabled \ - -Dmemgraph_include_dir=/usr/include/memgraph -ninja -C build-local-memgraph +meson compile -C build-memgraph ``` -The module artifact is `sync.so`. Install it into Memgraph's query module -directory, then load it: +The Linux artifact is `build-memgraph/sync.so`; macOS produces the native +shared-module equivalent. Install with Meson, or place that file in the +server's query-module directory and load it: ```cypher CALL mg.load("sync"); CALL mg.procedures() YIELD name +WITH name WHERE name STARTS WITH "sync." -RETURN name -ORDER BY name; +RETURN name ORDER BY name; ``` -To smoke-test a local server after loading the module: +Expected procedure names: + +```text +sync.explain_plan +sync.mark_dirty +sync.plan_disambiguate +sync.plan_sync +sync.prepare_model +sync.validate_update +``` + +Run the local integration test with an already installed Memgraph: ```sh -MEMGRAPH_PORT=7687 sh scripts/memgraph_local_smoke.sh +sh scripts/memgraph_local_smoke.sh build-memgraph ``` -The smoke test uses the temporary model name `sync_kgraph_smoke` and deletes -only nodes with that exact `model` property before and after the test. +It starts a separate Memgraph process on a temporary port with private data, +log, and module directories. It never connects to or modifies the normal +Memgraph instance on port 7687. -## Memgraph View Contract +## Manual View Contract -The application schema is not rewritten. A deployment manually materializes the -automaton view into Sync-KGraph's namespace: +Run `cypher/install_schema.cypher` once. The application schema is otherwise +untouched. Create dedicated view nodes and relationships for each model: ```cypher -(:SyncModel {model, generation, dirty}) -(:SyncState {model, state_key, state_id, base_id?}) -(:SyncLetter {model, letter, letter_id}) -(:SyncState)-[:SYNC_TRANS {model, letter}]->(:SyncState) +(:SyncModel { + model, generation, dirty, prepared_generation? +}) +(:SyncState { + model, state_key, state_id?, semantic_ref?, orientation? +}) +(:SyncAction { + model, action_key, action_id? +}) +(:SyncOutput { + model, output_key, output_id? +}) +(:SyncState)-[:SYNC_TRANS { + model, action_key +}]->(:SyncState) +(:SyncState)-[:SYNC_OBS { + model, action_key +}]->(:SyncOutput) ``` -The native module writes auxiliary view objects: +Keys must be unique within their domain and model. For every state/action pair, +there must be exactly one `SYNC_TRANS` and one `SYNC_OBS`. An observation is the +output emitted for the source state and selected action. Optional numeric IDs +only control stable ordering; keys are the public interface. -```cypher -(:SyncPair {model, pair_id, first_key, second_key, distance, has_witness, - witness, next_pair, generation}) -(:SyncPair)-[:PAIR_NEXT {model, letter, letter_id}]->(:SyncPair) -(:SyncPair)-[:PAIR_PRE {model, letter, letter_id}]->(:SyncPair) -(:SyncSubset {model, mode, target_key, subset_key, word, size, - word_length, generation}) -``` +The mapping is deliberately manual because only the database owner knows how +application entities, orientations, commands, and sensor abstractions form the +automaton. Prefer separate `SyncState` nodes linked by `semantic_ref` or an +application-owned relationship instead of adding `SyncState` to application +nodes. The supplied uninstall script deletes nodes in the Sync-KGraph +namespace. + +After creating or changing the view: -Run `cypher/install_schema.cypher` once for indexes. Then map existing graph -objects into `SyncState`, map symbolic commands into `SyncLetter`, and create -one `SYNC_TRANS` relationship for every `(state, letter)` pair. The C module -expects the materialized view to be complete and deterministic unless -`sync.build_model(model, true)` is used to complete missing transitions with a -sink state in memory. +1. Set `dirty: true` and advance `generation`, or call + `sync.mark_dirty(model)` for an existing model. +2. Call `sync.prepare_model(model, materialize_pair_edges)`. +3. Store the returned generation with every plan. +4. Reject or replan work when monitor output is `STALE_GENERATION`. -Primary procedures: +`prepare_model` validates the strict Mealy model and always persists one +`SyncPair` record for every unordered pair with repetition. Passing `true` also +persists `PAIR_NEXT` and `PAIR_PRE` relationships for inspection. Planning uses +the compact pair records, so edge materialization is optional. + +The trigger file is a template, not a generic installed trigger. Adapt its +predicate to the application labels and relationships that feed each model. A +schema-agnostic trigger cannot identify the affected model safely. + +## Procedure Interface ```cypher -CALL sync.validate_model(model) -CALL sync.build_pair_oracle(model, materialize) -CALL sync.word_for_set(model, state_keys, target_keys, mode, budget) -CALL sync.word_to_target(model, state_keys, target_keys, mode, budget) -CALL sync.expand_cache(model, target_keys, mode, budget) -CALL sync.explain(model, state_keys, word) +CALL sync.prepare_model(model, materialize_pair_edges = false) +CALL sync.plan_sync(model, hypotheses, budget) +CALL sync.plan_disambiguate(model, hypotheses, bound, budget) +CALL sync.explain_plan(model, generation, hypotheses, word) +CALL sync.validate_update( + model, generation, hypotheses, word, completed_steps, + reported_hypotheses, localizer_available = true) CALL sync.mark_dirty(model) ``` -The module does not execute arbitrary user Cypher strings. Compute hypotheses -with normal Cypher, collect their `state_key` values, then pass that list to -`sync.word_for_set` or `sync.word_to_target`. The `materialize` argument to -`sync.build_pair_oracle` defaults to `true`. +`hypotheses`, `reported_hypotheses`, and returned supports are lists of +`state_key` strings. Words are lists of `action_key` strings. `budget` limits +search expansions; `bound` is the required worst-case output-branch support +size. A bound of one requests a homing word. -## Worked Example +Planner calls return an `outcome` of `PLAN`, `ALREADY_SATISFIED`, `NO_PLAN`, or +`RESOURCE_BOUND`, and a `method` of `PAIR_MERGE`, `PAIR_RESOLUTION`, +`PARTITION_BFS`, or `NONE`. Dirty or unprepared models are rejected. -Load the office automaton: +The public C API is in `include/sync_kgraph/sync.h`. It exposes the same +automaton builder, pair oracle, planners, explanation visitor, and monitor +without requiring Memgraph. -```cypher -\i examples/office/00_reset_and_load.cypher -``` +## Worked Warehouse Example -It resets only Sync-KGraph view objects with `model: "office"`, then creates -states `A`, `B`, `C` and letters `north`, `east`. +The example maps two ambiguous bays, two corridor poses, and one dock pose. +Run each numbered file with `mgconsole`, or execute the queries shown below. -Transitions: +### 1. Install The Schema -```text -north: A -> B, B -> C, C -> C -east: A -> A, B -> B, C -> C +```sh +mgconsole < cypher/install_schema.cypher ``` -Validate the view: - -```cypher -CALL sync.validate_model("office") -YIELD ok, code, message, states, letters, transitions -RETURN ok, code, message, states, letters, transitions; -``` +Expected: nine indexes are created and no data rows are returned. Reuse these +indexes for every mapped model. -Expected: +### 2. Load The Manual View -```text -ok: true -code: "OK" -message: "model is complete and deterministic" -states: 3 -letters: 2 -transitions: 6 +```sh +mgconsole < examples/warehouse/00_reset_and_load.cypher ``` -Build/check the pair oracle: +Verify the mapping: ```cypher -CALL sync.build_pair_oracle("office") -YIELD status, pairs, pair_edges, mergeable_pairs, materialized, generation -RETURN status, pairs, pair_edges, mergeable_pairs, materialized, generation; +MATCH (m:SyncModel {model: "warehouse"}) +OPTIONAL MATCH (s:SyncState {model: "warehouse"}) +WITH m, count(s) AS states +OPTIONAL MATCH (a:SyncAction {model: "warehouse"}) +WITH m, states, count(a) AS actions +OPTIONAL MATCH (o:SyncOutput {model: "warehouse"}) +RETURN m.generation AS generation, m.dirty AS dirty, + states, actions, count(o) AS outputs; ``` Expected: ```text -status: "OK" -pairs: 6 -pair_edges: 12 -mergeable_pairs: 6 -materialized: true -generation: 0 +generation: 1, dirty: true, states: 5, actions: 4, outputs: 4 ``` -Inspect the materialized pair graph: +The view contains 20 transitions and 20 observations, one of each per +state/action cell. + +### 3. Prepare The Model ```cypher -MATCH (p:SyncPair {model: "office"}) -RETURN count(p) AS pairs; +CALL sync.prepare_model("warehouse", true) +YIELD status, generation, states, actions, outputs, transitions, pairs, + pair_edges, mergeable_pairs, resolvable_pairs, materialized_pair_edges +RETURN status, generation, states, actions, outputs, transitions, pairs, + pair_edges, mergeable_pairs, resolvable_pairs, materialized_pair_edges; ``` Expected: ```text -pairs: 6 +"OK", 1, 5, 4, 4, 20, 15, 60, 15, 15, true ``` +There are 15 unordered state pairs with repetition and 60 pair/action edges. +Because edge materialization was enabled, the graph contains 15 `SyncPair`, 60 +`PAIR_NEXT`, and 60 `PAIR_PRE` records. + +### 4. Plan Synchronization + ```cypher -MATCH (:SyncPair {model: "office"})-[r:PAIR_NEXT {model: "office"}]-> - (:SyncPair {model: "office"}) -RETURN count(r) AS pair_next; +CALL sync.plan_sync( + "warehouse", ["west_bay:east", "east_bay:west"], 64) +YIELD status, outcome, method, word, length, final_state_key, + final_support_size, generation +RETURN status, outcome, method, word, length, final_state_key, + final_support_size, generation; ``` Expected: ```text -pair_next: 12 +"OK", "PLAN", "PAIR_MERGE", ["to_corridor", "go_west"], 2, +"dock:north", 1, 1 ``` -Ask for a word from hypothesis `["A", "B"]` to target `["C"]`: +### 5. Plan Disambiguation ```cypher -CALL sync.word_to_target("office", ["A", "B"], ["C"], "REACH_AND_SYNC", 64) -YIELD status, word, length, final_state_key, generation -RETURN status, word, length, final_state_key, generation; +CALL sync.plan_disambiguate( + "warehouse", ["west_bay:east", "east_bay:west"], 1, 64) +YIELD status, outcome, method, word, length, best_support_size, + worst_support_size, branch_count, homing, generation +RETURN status, outcome, method, word, length, best_support_size, + worst_support_size, branch_count, homing, generation; ``` Expected: ```text -status: "PAIR_GREEDY_TARGETED" -word: ["north", "north"] -length: 2 -final_state_key: "C" -generation: 0 +"OK", "PLAN", "PAIR_RESOLUTION", ["to_corridor"], 1, +1, 1, 2, true, 1 ``` -Explain the returned word: +The two bays emit different landmark outputs under `to_corridor`, so one +action partitions the initial support into two singleton branches. + +### 6. Explain The Synchronizing Word ```cypher -CALL sync.explain("office", ["A", "B"], ["north", "north"]) -YIELD step, letter, active_state_keys -RETURN step, letter, active_state_keys -ORDER BY step; +CALL sync.explain_plan( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"]) +YIELD step, action, predicted_hypotheses, output_trace, branch_hypotheses +RETURN step, action, predicted_hypotheses, output_trace, branch_hypotheses +ORDER BY step, output_trace; ``` -Expected: +Expected rows: ```text -step: 0, letter: "", active_state_keys: ["A", "B"] -step: 1, letter: "north", active_state_keys: ["B", "C"] -step: 2, letter: "north", active_state_keys: ["C"] +0, "", [west_bay:east, east_bay:west], [], + [west_bay:east, east_bay:west] +1, "to_corridor", [corridor_w:east, corridor_e:west], + [west_landmark], [corridor_w:east] +1, "to_corridor", [corridor_w:east, corridor_e:west], + [east_landmark], [corridor_e:west] +2, "go_west", [dock:north], [west_landmark, dock], [dock:north] +2, "go_west", [dock:north], [east_landmark, dock], [dock:north] ``` -Expand the reverse-subset cache for target `C`: +### 7. Validate A Localization Update ```cypher -CALL sync.expand_cache("office", ["C"], "REACH_AND_SYNC", 64) -YIELD status, expanded, cache_size -RETURN status, expanded, cache_size; +CALL sync.validate_update( + "warehouse", 1, + ["west_bay:east", "east_bay:west"], + ["to_corridor", "go_west"], 1, + ["corridor_w:east", "corridor_e:west"], true) +YIELD status, decision, expected_hypotheses, unexpected_hypotheses, generation +RETURN status, decision, expected_hypotheses, unexpected_hypotheses, generation; ``` Expected: ```text -status: "OK" -expanded: 3 -cache_size: 3 +"OK", "CONTINUE", [corridor_w:east, corridor_e:west], [], 1 ``` -Inspect the persisted cache: +Reporting only one expected corridor returns `REPLAN`; reporting `dock:north` +at this step returns `MODEL_VIOLATION`; passing an unavailable localizer returns +`WAIT`. + +### 8. Invalidate Old Plans ```cypher -MATCH (s:SyncSubset {model: "office", mode: "REACH_AND_SYNC", target_key: "C"}) -RETURN count(s) AS subsets; +CALL sync.mark_dirty("warehouse") YIELD status, generation +RETURN status, generation; ``` Expected: ```text -subsets: 3 +"DIRTY", 2 ``` -The same example can be checked without Memgraph: +Planning is now rejected until `sync.prepare_model("warehouse", false)` +succeeds. A monitor call carrying generation 1 returns: -```sh -./build/sync-kgraph-cli --example office +```text +status: "OK", decision: "STALE_GENERATION", generation: 2 ``` -## Quality Gates +## Quality And Releases -CI runs `clang-format`, `clang-tidy`, `meson test`, Valgrind leak checks, -`gcovr` with a minimum line coverage gate of 75%, and a Dockerized Memgraph -module smoke test. Release tags publish: +```sh +sh scripts/check-format.sh +sh scripts/run-clang-tidy.sh build-tidy +sh scripts/valgrind.sh build-valgrind +sh scripts/coverage.sh build-coverage /usr/include/memgraph +``` -- `sync-kgraph-linux-x86_64.tar.gz` -- `sync-kgraph-macos-arm64.tar.gz` +CI treats all Clang diagnostics as errors, runs every unit test under Valgrind +with all leak kinds fatal, compiles against multiple Memgraph C API versions, +and runs the full Memgraph integration contract. Coverage includes every +production C source file, including the CLI and Memgraph adapter, and fails +below 75% line coverage. -Each bundle contains the native binary artifacts, Cypher scripts, the GSS view, -the office example, `README.md`, `LICENSE`, and a SHA-256 checksum. +Release tags publish `linux-x86_64` and native `macos-arm64` archives. Each +archive includes the CLI, C library and header, Memgraph module, Cypher mapping +scripts, Memgraph Lab GSS view, worked example, license, and SHA-256 checksum. diff --git a/scripts/package_release.sh b/scripts/package_release.sh index 422fc5f..321b46a 100644 --- a/scripts/package_release.sh +++ b/scripts/package_release.sh @@ -12,9 +12,16 @@ outdir="$3" stagedir="$outdir/sync-kgraph-$target" rm -rf "$stagedir" -mkdir -p "$stagedir/bin" "$stagedir/lib" "$stagedir/cypher" "$stagedir/examples" "$stagedir/views" +mkdir -p \ + "$stagedir/bin" \ + "$stagedir/lib" \ + "$stagedir/include/sync_kgraph" \ + "$stagedir/cypher" \ + "$stagedir/examples" \ + "$stagedir/views" cp "$builddir/sync-kgraph-cli" "$stagedir/bin/" +cp "$builddir/libsync_kgraph.a" "$stagedir/lib/" if [ -f "$builddir/sync.so" ]; then cp "$builddir/sync.so" "$stagedir/lib/" elif [ -f "$builddir/sync.dylib" ]; then @@ -22,8 +29,9 @@ elif [ -f "$builddir/sync.dylib" ]; then fi cp README.md LICENSE "$stagedir/" +cp include/sync_kgraph/sync.h "$stagedir/include/sync_kgraph/" cp cypher/*.cypher "$stagedir/cypher/" -cp -R examples/office "$stagedir/examples/" +cp -R examples/warehouse "$stagedir/examples/" cp views/sync_automata.gss "$stagedir/views/" tar -C "$outdir" -czf "$outdir/sync-kgraph-$target.tar.gz" "sync-kgraph-$target"