Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions examples/pkg_resolver/c/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ as open or cyclic lists, invalid scalar values, and U+0000 report
The existing forward mode remains unchanged. The focused generated-C/SWI suite
is `tests/test_wam_c_atom_codes.pl`.

### Atom chars reverse-mode parity slice (2026-09-14)

The C runtime accepts `atom_chars(-Atom, +Chars)` for finite, fully bound proper
lists whose elements are single Unicode scalar character atoms. Both WAM list
cell layouts, including compiled `"[|]/2"` cons cells, are accepted. The atom
binding follows normal trail and backtracking semantics. Open, cyclic, or
improper lists, non-character elements, invalid UTF-8, and U+0000 are outside
this mode and report `WAM_ERR_UNSUPPORTED`; NUL-terminated C atoms cannot
represent U+0000. The focused generated-C/SWI suite is
`tests/test_wam_c_atom_chars.pl`.

## Reverse implementation history (2026-09-09)

This section records the earlier reverse implementation checkpoint. Its stale
Expand Down
88 changes: 82 additions & 6 deletions src/unifyweaver/targets/wam_c_target.pl
Original file line number Diff line number Diff line change
Expand Up @@ -5115,6 +5115,7 @@

/* The reverse mode accepts only a finite, fully bound proper code list. A
bounded walk also rejects cyclic lists without mutating the query. */
static bool wam_decode_single_utf8_char(const char *text, int *cp_out);
static bool wam_encode_single_utf8_char(int cp, char buf[5]);

static bool wam_atom_codes_to_text(WamState *state, WamValue *codes,
Expand Down Expand Up @@ -5324,15 +5325,90 @@
return cell->tag == VAL_ATOM && cell->data.atom && strcmp(cell->data.atom, "[]") == 0;
}

/* atom_chars/2: forward atom-to-chars mode only.
Decompose atom text into a list of single-character atoms.
Unbound atom or non-atom inputs are diagnosed as WAM_ERR_UNSUPPORTED.
Unification failure rolls back trail and heap bindings made by this builtin. */
/* The reverse mode accepts only a finite, fully bound proper character list.
Each element must be an atom representing exactly one Unicode scalar value.
Both VAL_LIST and compiled "[|]/2" cons cells are supported.
Cyclic, open, improper, multi-character, invalid UTF-8, and U+0000 inputs
are rejected. */
static bool wam_atom_chars_to_text(WamState *state, WamValue *chars,
char **text_out) {
WamValue *cell = wam_deref_ptr(state, chars);
size_t bytes = 0;
int count = 0;
for (;;) {
if ((cell->tag == VAL_LIST || cell->tag == VAL_STR) &&
(cell->data.ref_addr < 0 || cell->data.ref_addr >= state->H))
return false;
int base = wam_cons_head_addr(state, cell);
if (base < 0) break;
if (++count > 262144 || base >= state->H - 1)
return false;
WamValue *head = wam_deref_ptr(state, &state->H_array[base]);
int cp = 0;
char encoded[5];
if (head->tag != VAL_ATOM || !head->data.atom ||
!wam_decode_single_utf8_char(head->data.atom, &cp) ||
!wam_encode_single_utf8_char(cp, encoded))
return false;
bytes += strlen(encoded);
cell = wam_deref_ptr(state, &state->H_array[base + 1]);
}
if (cell->tag != VAL_ATOM || !cell->data.atom ||
strcmp(cell->data.atom, "[]") != 0)
return false;

char *text = (char *)malloc(bytes + 1);
if (!text) return false;
size_t at = 0;
cell = wam_deref_ptr(state, chars);
for (;;) {
int base = wam_cons_head_addr(state, cell);
if (base < 0) break;
WamValue *head = wam_deref_ptr(state, &state->H_array[base]);
int cp = 0;
char encoded[5];
if (head->tag != VAL_ATOM || !head->data.atom ||
!wam_decode_single_utf8_char(head->data.atom, &cp) ||
!wam_encode_single_utf8_char(cp, encoded)) {
free(text);
return false;
}
size_t len = strlen(encoded);
memcpy(text + at, encoded, len);
at += len;
cell = wam_deref_ptr(state, &state->H_array[base + 1]);
}
text[at] = 0;
*text_out = text;
return true;
}

/* atom_chars/2: atom-to-chars and fully ground chars-to-atom modes.
Unsupported shapes diagnose WAM_ERR_UNSUPPORTED; unification failures
roll back trail and heap bindings made by this builtin. */
static bool wam_execute_atom_chars(WamState *state) {
WamValue *a1 = wam_deref_ptr(state, &state->A[0]);
if (val_is_unbound(*a1)) {
wam_set_unsupported_builtin(state, "atom_chars/2", 2);
return false;
char *text = NULL;
if (!wam_atom_chars_to_text(state, &state->A[1], &text)) {
wam_set_unsupported_builtin(state, "atom_chars/2", 2);
return false;
}
const char *interned = wam_intern_atom(state, text);
if (interned == text) {
free(text);
return false;
}
free(text);
int trail_mark = state->TR;
int heap_mark = state->H;
WamValue atom_val = val_atom(interned);
if (!wam_unify(state, &state->A[0], &atom_val)) {
unwind_trail(state, trail_mark);
state->H = heap_mark;
return false;
}
return true;
}
if (a1->tag != VAL_ATOM || !a1->data.atom) {
wam_set_unsupported_builtin(state, "atom_chars/2", 2);
Expand Down
121 changes: 110 additions & 11 deletions tests/test_wam_c_atom_chars.pl
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
% Copyright (c) 2026 John William Creighton (@s243a)
%
% Focused C atom_chars/2: generated/compiled C vs SWI for forward
% atom-to-chars mode (ASCII, empty, multibyte code points).
% atom-to-chars and fully bound reverse chars-to-atom modes.
% Compatible and incompatible prebound outputs, rollback after mismatch,
% caller continuation, and variable identity are verified.
% Unbound atom, unsupported reverse mode, and non-atom inputs
% Both-unbound, malformed reverse lists, and non-atom inputs
% explicitly diagnose WAM_ERR_UNSUPPORTED, not silent logical failure.
%
% swipl -q -g run_tests -t halt tests/test_wam_c_atom_chars.pl
% swipl --on-error=halt -q -g run_tests -t halt tests/test_wam_c_atom_chars.pl

:- use_module('../src/unifyweaver/targets/wam_c_target').
:- use_module('../src/unifyweaver/targets/wam_target').
Expand All @@ -22,6 +22,7 @@
:- dynamic user:wam_atom_chars_continuation/3.
:- dynamic user:wam_atom_chars_bind_control/1.
:- dynamic user:wam_atom_chars_backtrack/1.
:- dynamic user:wam_atom_chars_reverse_backtrack/1.
:- dynamic user:wam_atom_chars_mismatch_positive/1.
:- dynamic user:wam_atom_chars_mismatch_rollback/1.

Expand Down Expand Up @@ -56,6 +57,7 @@
retractall(user:wam_atom_chars_continuation(_, _, _)),
retractall(user:wam_atom_chars_bind_control(_)),
retractall(user:wam_atom_chars_backtrack(_)),
retractall(user:wam_atom_chars_reverse_backtrack(_)),
retractall(user:wam_atom_chars_mismatch_positive(_)),
retractall(user:wam_atom_chars_mismatch_rollback(_)).

Expand All @@ -70,6 +72,10 @@
( atom_chars(foo, C), C == [c, c], fail
; ( var(C) -> Out = restored ; Out = bad )
), Out == restored)),
assertz((user:wam_atom_chars_reverse_backtrack(Out) :-
( atom_chars(A, [a, b]), A == ab, fail
; ( var(A) -> Out = restored ; Out = bad )
), Out == restored)),
assertz((user:wam_atom_chars_mismatch_positive(Out) :-
atom_chars(hello, C), Out = C)),
assertz((user:wam_atom_chars_mismatch_rollback(Out) :-
Expand Down Expand Up @@ -113,6 +119,8 @@
user:wam_atom_chars_bind_control([f, o, o]).
token_swi(backtrack_rollback, ok, backtrack_ok) :-
user:wam_atom_chars_backtrack(restored).
token_swi(reverse_backtrack_rollback, ok, reverse_backtrack_ok) :-
user:wam_atom_chars_reverse_backtrack(restored).
token_swi(mismatch_positive, ok, mismatch_positive_ok) :-
user:wam_atom_chars_mismatch_positive([h, e, l, l, o]).
token_swi(mismatch_rollback, ok, mismatch_rollback_ok) :-
Expand All @@ -123,7 +131,24 @@
S1 == start, S2 == done.
token_swi(repeated_mismatch_rollback, ok, repeated_rollback_ok).
token_swi(unbound_atom, ok, unbound_atom_ok).
token_swi(reverse_mode, ok, reverse_mode_ok).
token_swi(reverse_mode, ok, reverse_mode_ok) :-
atom_chars(Atom, [a, b]), Atom == ab.
token_swi(reverse_ascii, ok, reverse_ascii_ok) :-
atom_chars(Atom, [h, e, l, l, o]), Atom == hello.
token_swi(reverse_empty, ok, reverse_empty_ok) :-
atom_chars(Atom, []), Atom == ''.
token_swi(reverse_unicode, ok, reverse_unicode_ok) :-
atom_chars(Atom, ['λ', '🚀']), Atom == 'λ🚀'.
token_swi(reverse_multibyte_cjk, ok, reverse_cjk_ok) :-
atom_chars(Atom, ['日', '本', '語']), Atom == '日本語'.
token_swi(reverse_bad_char, ok, reverse_bad_char_ok).
token_swi(reverse_empty_char, ok, reverse_empty_char_ok).
token_swi(reverse_open_list, ok, reverse_open_list_ok).
token_swi(reverse_cyclic_list, ok, reverse_cyclic_list_ok).
token_swi(reverse_improper_list, ok, reverse_improper_list_ok).
token_swi(reverse_invalid_utf8, ok, reverse_invalid_utf8_ok).
token_swi(reverse_unbound_elem, ok, reverse_unbound_elem_ok).
token_swi(reverse_non_atom, ok, reverse_non_atom_ok).
token_swi(compound_input, ok, compound_ok).
token_swi(nonempty_list, ok, nonempty_list_ok).
token_swi(integer_input, ok, integer_ok).
Expand Down Expand Up @@ -279,6 +304,7 @@
compile_one(user:wam_atom_chars_continuation/3, ContCode),
compile_one(user:wam_atom_chars_bind_control/1, BindCode),
compile_one(user:wam_atom_chars_backtrack/1, BackCode),
compile_one(user:wam_atom_chars_reverse_backtrack/1, RevBackCode),
compile_one(user:wam_atom_chars_mismatch_positive/1, MisPosCode),
compile_one(user:wam_atom_chars_mismatch_rollback/1, MismatchCode),
compile_wam_runtime_to_c([], RuntimeCode),
Expand All @@ -291,7 +317,7 @@
format(atom(ExePath), '~w_bin', [TmpBase]),
write_text_file(RuntimePath, RuntimeCode),
atomic_list_concat([QCode, "\n\n", ContCode, "\n\n", BindCode, "\n\n",
BackCode, "\n\n", MisPosCode, "\n\n", MismatchCode],
BackCode, "\n\n", RevBackCode, "\n\n", MisPosCode, "\n\n", MismatchCode],
AllPredCode),
format(atom(PredTranslationUnit), '#include "wam_runtime.h"~n~n~w', [AllPredCode]),
write_text_file(PredPath, PredTranslationUnit),
Expand All @@ -316,10 +342,15 @@
prebound_mismatch_nonlist,
prebound_empty_mismatch,
aliased_var_mismatch, preserve_input, reference_chain,
repeated_calls, bind_control, backtrack_rollback,
repeated_calls, bind_control, backtrack_rollback, reverse_backtrack_rollback,
mismatch_positive, mismatch_rollback, c_unifier_rollback,
caller_continuation, repeated_mismatch_rollback,
unbound_atom, reverse_mode, compound_input, nonempty_list,
unbound_atom, reverse_mode, reverse_ascii, reverse_empty,
reverse_unicode, reverse_multibyte_cjk, reverse_bad_char,
reverse_empty_char, reverse_open_list,
reverse_cyclic_list, reverse_improper_list, reverse_invalid_utf8,
reverse_unbound_elem, reverse_non_atom,
compound_input, nonempty_list,
integer_input, distinguish_mismatch_vs_unsupported,
prebound_heap_stable, malformed_utf8, heap_alloc_failure],
findall(Id, (member(Id, Ground), \+ compare_ground(Id, CCases)), GroundBads),
Expand All @@ -339,6 +370,7 @@
void setup_wam_atom_chars_continuation_3(WamState *state);
void setup_wam_atom_chars_bind_control_1(WamState *state);
void setup_wam_atom_chars_backtrack_1(WamState *state);
void setup_wam_atom_chars_reverse_backtrack_1(WamState *state);
void setup_wam_atom_chars_mismatch_positive_1(WamState *state);
void setup_wam_atom_chars_mismatch_rollback_1(WamState *state);

Expand Down Expand Up @@ -575,6 +607,7 @@
setup_wam_atom_chars_continuation_3(&state);
setup_wam_atom_chars_bind_control_1(&state);
setup_wam_atom_chars_backtrack_1(&state);
setup_wam_atom_chars_reverse_backtrack_1(&state);
setup_wam_atom_chars_mismatch_positive_1(&state);
setup_wam_atom_chars_mismatch_rollback_1(&state);

Expand Down Expand Up @@ -829,15 +862,81 @@
wam_clear_error(&state);
}

/* 21. Reverse mode (unbound atom, bound chars) -> WAM_ERR_UNSUPPORTED */
/* 21. Reverse mode (unbound atom, bound chars). */
{
static const char * const ab_chars[] = {"a", "b"};
WamValue chars = build_char_list(&state, ab_chars, 2);
int rc = run_atom_chars(&state, val_unbound("A"), chars);
int ok = (rc == WAM_ERR_UNSUPPORTED && state.error == WAM_ERR_UNSUPPORTED &&
state.error_op != NULL && strcmp(state.error_op, "atom_chars/2") == 0);
WamValue a = make_out_ref(&state, "A");
int rc = run_atom_chars(&state, a, chars);
int ok = rc == 0 && state.error == 0 &&
same_atom(wam_deref_ptr(&state, &a), "ab");
emit_token("reverse_mode", ok ? "ok" : "fail",
ok ? "reverse_mode_ok" : "reverse_mode_bad");
}

/* Reverse ASCII, empty, and multibyte lists use the generated C path. */
{
const char * const ascii[] = {"h", "e", "l", "l", "o"};
const char * const unicode[] = {"λ", "🚀"};
const char * const cjk[] = {"日", "本", "語"};
const char * const *items[] = {ascii, NULL, unicode, cjk};
const int sizes[] = {5, 0, 2, 3};
const char * const names[] = {"reverse_ascii", "reverse_empty", "reverse_unicode", "reverse_multibyte_cjk"};
const char * const atoms[] = {"hello", "", "λ🚀", "日本語"};
const char * const tokens[] = {"reverse_ascii_ok", "reverse_empty_ok", "reverse_unicode_ok", "reverse_cjk_ok"};
for (int i = 0; i < 4; i++) {
WamValue a = make_out_ref(&state, "A");
WamValue list = build_char_list(&state, items[i], sizes[i]);
int rc = run_atom_chars(&state, a, list);
int ok = rc == 0 && state.error == 0 && same_atom(wam_deref_ptr(&state, &a), atoms[i]);
emit_token(names[i], ok ? "ok" : "fail", ok ? tokens[i] : "reverse_bad");
}
}
{
WamValue out = make_out_ref(&state, "Out");
WamValue args[1] = {out};
int rc = wam_run_predicate(&state, "wam_atom_chars_reverse_backtrack/1", args, 1);
int ok = rc == 0 && state.error == 0 &&
same_atom(wam_deref_ptr(&state, &out), "restored");
emit_token("reverse_backtrack_rollback", ok ? "ok" : "fail",
ok ? "reverse_backtrack_ok" : "reverse_backtrack_bad");
}

/* Malformed reverse inputs are explicit unsupported diagnostics. */
{
char invalid[2] = {(char)0x80, 0};
const char * const bad[] = {"ab", "", invalid};
const char * const names[] = {"reverse_bad_char", "reverse_empty_char", "reverse_invalid_utf8"};
const char * const tokens[] = {"reverse_bad_char_ok", "reverse_empty_char_ok", "reverse_invalid_utf8_ok"};
for (int i = 0; i < 3; i++) {
WamValue list = cons(&state, val_atom(bad[i]), val_atom("[]"));
state.A[0] = val_unbound("A");
state.A[1] = list;
int ok = !wam_execute_builtin(&state, "atom_chars/2", 2) && state.error == WAM_ERR_UNSUPPORTED;
emit_token(names[i], ok ? "ok" : "fail", ok ? tokens[i] : "reverse_bad");
wam_clear_error(&state);
}
WamValue open = cons(&state, val_atom("a"), val_unbound("Tail"));
WamValue improper = cons(&state, val_atom("a"), val_atom("tail"));
WamValue unbound = cons(&state, val_unbound("Head"), val_atom("[]"));
WamValue nonatom = cons(&state, val_int(1), val_atom("[]"));
WamValue lists[] = {open, improper, unbound, nonatom};
const char * const list_names[] = {"reverse_open_list", "reverse_improper_list", "reverse_unbound_elem", "reverse_non_atom"};
const char * const list_tokens[] = {"reverse_open_list_ok", "reverse_improper_list_ok", "reverse_unbound_elem_ok", "reverse_non_atom_ok"};
for (int i = 0; i < 4; i++) {
state.A[0] = val_unbound("A");
state.A[1] = lists[i];
int ok = !wam_execute_builtin(&state, "atom_chars/2", 2) && state.error == WAM_ERR_UNSUPPORTED;
emit_token(list_names[i], ok ? "ok" : "fail", ok ? list_tokens[i] : "reverse_bad");
wam_clear_error(&state);
}
WamValue cycle = cons(&state, val_atom("a"), val_atom("[]"));
state.H_array[cycle.data.ref_addr + 1] = cycle;
state.A[0] = val_unbound("A");
state.A[1] = cycle;
int ok = !wam_execute_builtin(&state, "atom_chars/2", 2) && state.error == WAM_ERR_UNSUPPORTED;
emit_token("reverse_cyclic_list", ok ? "ok" : "fail",
ok ? "reverse_cyclic_list_ok" : "reverse_bad");
wam_clear_error(&state);
}

Expand Down
Loading