From 5e4c7350a940a0b177dfaba78b81c7aabf0894dd Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sat, 25 Apr 2026 18:50:37 +0200 Subject: [PATCH] :goat: ORDINA with comparitor --- README.md | 13 +++++- centvrion/ast_nodes.py | 54 ++++++++++++++++------ centvrion/compiler/emit_expr.py | 9 +++- centvrion/compiler/runtime/cent_runtime.c | 49 ++++++++++++++++++++ centvrion/compiler/runtime/cent_runtime.h | 1 + snippets/ordina_cmp.cent | 4 ++ snippets/ordina_cmp.png | Bin 0 -> 19133 bytes tests/03_test_builtins.py | 10 ++++ tests/12_test_failures.py | 4 ++ 9 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 snippets/ordina_cmp.cent create mode 100644 snippets/ordina_cmp.png diff --git a/README.md b/README.md index 6e69900..8d2a6e0 100644 --- a/README.md +++ b/README.md @@ -357,9 +357,18 @@ Returns the length of `array` (element count), `string` (character count), or `d Returns the keys of `dict` as an array. ### ORDINA -`ORDINA(array)` +`ORDINA(array)` or `ORDINA(array, comparator)` -Sorts an array in ascending order. Returns a new sorted array. All elements must be the same type — integers, fractions, or strings. Integers and fractions sort numerically; strings sort lexicographically. +Sorts an array. Returns a new sorted array; the original is unchanged. + +Without a comparator, sorts in ascending order. All elements must be the same type — integers, fractions, or strings. Integers and fractions sort numerically; strings sort lexicographically. + +With a comparator, the type-uniformity rule is dropped — the comparator decides ordering. The comparator must be a function of exactly two parameters and must return `VERAX`. `comparator(a, b)` returns `VERITAS` iff `a` should come **before** `b`; two elements are treated as equal when both `comparator(a, b)` and `comparator(b, a)` are `FALSITAS`. + +![ORDINA with comparator](snippets/ordina_cmp.png) +``` +> [V III II I] +``` ### ADDE `ADDE(array, value)` diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index 95e8ded..96e65f8 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -1,3 +1,4 @@ +import functools import http.server import re import time @@ -1328,6 +1329,22 @@ class TemptaStatement(Node): return vtable, last_val +def _call_func(func: ValFunc, args: list, vtable: dict, callee_desc: str = "function"): + if len(args) != len(func.params): + raise CentvrionError( + f"{callee_desc} expects {len(func.params)} argument(s), got {len(args)}" + ) + func_vtable = vtable.copy() + for i, param in enumerate(func.params): + func_vtable[param.name] = args[i] + func_vtable["#return"] = None + for statement in func.body: + func_vtable, _ = statement.eval(func_vtable) + if func_vtable["#return"] is not None: + return func_vtable["#return"] + return ValNul() + + class Invoca(Node): def __init__(self, callee, parameters) -> None: self.callee = callee @@ -1354,21 +1371,9 @@ class Invoca(Node): callee_desc = (self.callee.name if isinstance(self.callee, ID) else "expression") raise CentvrionError(f"{callee_desc} is not a function") - if len(params) != len(func.params): - callee_desc = (self.callee.name - if isinstance(self.callee, ID) else "FVNCTIO") - raise CentvrionError( - f"{callee_desc} expects {len(func.params)} argument(s), got {len(params)}" - ) - func_vtable = vtable.copy() - for i, param in enumerate(func.params): - func_vtable[param.name] = params[i] - func_vtable["#return"] = None - for statement in func.body: - func_vtable, _ = statement.eval(func_vtable) - if func_vtable["#return"] is not None: - return vtable, func_vtable["#return"] - return vtable, ValNul() + callee_desc = (self.callee.name + if isinstance(self.callee, ID) else "FVNCTIO") + return vtable, _call_func(func, params, vtable, callee_desc) class BuiltIn(Node): @@ -1546,11 +1551,30 @@ class BuiltIn(Node): d[k.value()] = v return vtable, ValDict(d) case "ORDINA": + if not 1 <= len(params) <= 2: + raise CentvrionError("ORDINA takes 1 or 2 arguments") if not isinstance(params[0], ValList): raise CentvrionError("ORDINA requires an array") items = list(params[0].value()) if not items: return vtable, ValList([]) + if len(params) == 2: + cmp = params[1] + if not isinstance(cmp, ValFunc): + raise CentvrionError("ORDINA comparator must be a function") + if len(cmp.params) != 2: + raise CentvrionError("ORDINA comparator must take 2 arguments") + def adapter(a, b): + r = _call_func(cmp, [a, b], vtable, "ORDINA comparator") + if not isinstance(r, ValBool): + raise CentvrionError("ORDINA comparator must return VERAX") + if r.value(): + return -1 + r2 = _call_func(cmp, [b, a], vtable, "ORDINA comparator") + if not isinstance(r2, ValBool): + raise CentvrionError("ORDINA comparator must return VERAX") + return 1 if r2.value() else 0 + return vtable, ValList(sorted(items, key=functools.cmp_to_key(adapter))) all_numeric = all(isinstance(i, (ValInt, ValFrac)) for i in items) all_string = all(isinstance(i, ValStr) for i in items) if not (all_numeric or all_string): diff --git a/centvrion/compiler/emit_expr.py b/centvrion/compiler/emit_expr.py index e7c03af..bed2b2f 100644 --- a/centvrion/compiler/emit_expr.py +++ b/centvrion/compiler/emit_expr.py @@ -311,7 +311,14 @@ def _emit_builtin(node, ctx): lines.append(f"CentValue {tmp} = cent_dict_keys({param_vars[0]});") case "ORDINA": - lines.append(f"CentValue {tmp} = cent_ordina({param_vars[0]});") + if len(param_vars) == 1: + lines.append(f"CentValue {tmp} = cent_ordina({param_vars[0]});") + elif len(param_vars) == 2: + lines.append( + f"CentValue {tmp} = cent_ordina_cmp({param_vars[0]}, {param_vars[1]}, _scope);" + ) + else: + raise CentvrionError("ORDINA takes 1 or 2 arguments") case "ADDE": lines.append(f"CentValue {tmp} = cent_adde({param_vars[0]}, {param_vars[1]});") diff --git a/centvrion/compiler/runtime/cent_runtime.c b/centvrion/compiler/runtime/cent_runtime.c index 5bfc918..44b003d 100644 --- a/centvrion/compiler/runtime/cent_runtime.c +++ b/centvrion/compiler/runtime/cent_runtime.c @@ -848,6 +848,55 @@ CentValue cent_ordina(CentValue lst) { return result; } +/* User-comparator sort: single-threaded runtime, so the active comparator + and its calling scope are stashed in file-scope statics. Save/restore + them around the qsort call so nested ORDINA(..., cmp) calls inside a + comparator still work. */ +static CentValue _cmp_active; +static CentScope _cmp_scope; + +static int _ordina_user_comparator(const void *a, const void *b) { + const CentValue *va = (const CentValue *)a; + const CentValue *vb = (const CentValue *)b; + CentScope s1 = cent_scope_copy(&_cmp_scope); + cent_scope_set(&s1, _cmp_active.fnval.param_names[0], *va); + cent_scope_set(&s1, _cmp_active.fnval.param_names[1], *vb); + CentValue r1 = _cmp_active.fnval.fn(s1); + if (r1.type != CENT_BOOL) + cent_type_error("'ORDINA' comparator must return VERAX"); + if (r1.bval) return -1; + CentScope s2 = cent_scope_copy(&_cmp_scope); + cent_scope_set(&s2, _cmp_active.fnval.param_names[0], *vb); + cent_scope_set(&s2, _cmp_active.fnval.param_names[1], *va); + CentValue r2 = _cmp_active.fnval.fn(s2); + if (r2.type != CENT_BOOL) + cent_type_error("'ORDINA' comparator must return VERAX"); + return r2.bval ? 1 : 0; +} + +CentValue cent_ordina_cmp(CentValue lst, CentValue cmp, CentScope scope) { + if (lst.type != CENT_LIST) + cent_type_error("'ORDINA' requires a list"); + if (cmp.type != CENT_FUNC) + cent_type_error("'ORDINA' comparator must be a function"); + if (cmp.fnval.param_count != 2) + cent_runtime_error("'ORDINA' comparator must take 2 arguments"); + int len = lst.lval.len; + CentValue result = cent_list_new(len); + for (int i = 0; i < len; i++) + cent_list_push(&result, lst.lval.items[i]); + if (len > 1) { + CentValue saved_cmp = _cmp_active; + CentScope saved_scope = _cmp_scope; + _cmp_active = cmp; + _cmp_scope = scope; + qsort(result.lval.items, len, sizeof(CentValue), _ordina_user_comparator); + _cmp_active = saved_cmp; + _cmp_scope = saved_scope; + } + return result; +} + static long _index_arg(CentValue idx, const char *name) { if (idx.type == CENT_INT) return idx.ival; diff --git a/centvrion/compiler/runtime/cent_runtime.h b/centvrion/compiler/runtime/cent_runtime.h index 0bdc7f2..fcc6d15 100644 --- a/centvrion/compiler/runtime/cent_runtime.h +++ b/centvrion/compiler/runtime/cent_runtime.h @@ -239,6 +239,7 @@ CentValue cent_senatus(CentValue *args, int n); /* SENATVS */ CentValue cent_typvs(CentValue v); /* TYPVS */ void cent_dormi(CentValue n); /* DORMI */ CentValue cent_ordina(CentValue lst); /* ORDINA */ +CentValue cent_ordina_cmp(CentValue lst, CentValue cmp, CentScope scope); /* ORDINA w/ comparator */ CentValue cent_adde(CentValue lst, CentValue v); /* ADDE */ CentValue cent_tolle(CentValue lst, CentValue idx); /* TOLLE */ CentValue cent_insere(CentValue lst, CentValue idx, CentValue v); /* INSERE */ diff --git a/snippets/ordina_cmp.cent b/snippets/ordina_cmp.cent new file mode 100644 index 0000000..d11069f --- /dev/null +++ b/snippets/ordina_cmp.cent @@ -0,0 +1,4 @@ +DEFINI gt (a, b) VT { + REDI (a PLVS b) +} +DIC (ORDINA([II, V, I, III], gt)) diff --git a/snippets/ordina_cmp.png b/snippets/ordina_cmp.png new file mode 100644 index 0000000000000000000000000000000000000000..d932c1f286e6a2f405fc1992166790a44670a9e1 GIT binary patch literal 19133 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