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 0000000..d932c1f Binary files /dev/null and b/snippets/ordina_cmp.png differ diff --git a/tests/03_test_builtins.py b/tests/03_test_builtins.py index 13b9ebf..eef1ece 100644 --- a/tests/03_test_builtins.py +++ b/tests/03_test_builtins.py @@ -76,6 +76,16 @@ builtin_tests = [ ("CVM FRACTIO\nORDINA([III, S, II])", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("III"), Fractio("S"), Numeral("II")])]))]), ValList([ValFrac(Fraction(1, 2)), ValInt(2), ValInt(3)])), # ORDINA: array passed via variable ("DESIGNA x VT [III, I, II]\nORDINA(x)", Program([], [Designa(ID("x"), DataArray([Numeral("III"), Numeral("I"), Numeral("II")])), ExpressionStatement(BuiltIn("ORDINA", [ID("x")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])), + # ORDINA: descending sort with named comparator + ("DEFINI gt (a, b) VT { REDI (a PLVS b) }\nORDINA([II, V, I, III], gt)", Program([], [Defini(ID("gt"), [ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "KEYWORD_PLVS")])]), ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("II"), Numeral("V"), Numeral("I"), Numeral("III")]), ID("gt")]))]), ValList([ValInt(5), ValInt(3), ValInt(2), ValInt(1)])), + # ORDINA: ascending sort with inline FVNCTIO comparator + ("ORDINA([V, I, III], FVNCTIO (a, b) VT { REDI (a MINVS b) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("V"), Numeral("I"), Numeral("III")]), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "KEYWORD_MINVS")])])]))]), ValList([ValInt(1), ValInt(3), ValInt(5)])), + # ORDINA: empty list with comparator + ("ORDINA([], FVNCTIO (a, b) VT { REDI (VERITAS) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([]), Fvnctio([ID("a"), ID("b")], [Redi([Bool(True)])])]))]), ValList([])), + # ORDINA: single element with comparator + ("ORDINA([VII], FVNCTIO (a, b) VT { REDI (VERITAS) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("VII")]), Fvnctio([ID("a"), ID("b")], [Redi([Bool(True)])])]))]), ValList([ValInt(7)])), + # ORDINA: comparator sorting two-element subarrays by first element + ("ORDINA([[II, I], [I, II]], FVNCTIO (a, b) VT { REDI (a[I] PLVS b[I]) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([DataArray([Numeral("II"), Numeral("I")]), DataArray([Numeral("I"), Numeral("II")])]), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ArrayIndex(ID("a"), Numeral("I")), ArrayIndex(ID("b"), Numeral("I")), "KEYWORD_PLVS")])])]))]), ValList([ValList([ValInt(2), ValInt(1)]), ValList([ValInt(1), ValInt(2)])])), # ADDE: append to non-empty ("ADDE([I, II], III)", Program([], [ExpressionStatement(BuiltIn("ADDE", [DataArray([Numeral("I"), Numeral("II")]), Numeral("III")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])), # ADDE: append to empty diff --git a/tests/12_test_failures.py b/tests/12_test_failures.py index 695087b..2a3cf84 100644 --- a/tests/12_test_failures.py +++ b/tests/12_test_failures.py @@ -71,6 +71,10 @@ error_tests = [ ("ORDINA(I)", CentvrionError), # ORDINA on non-array ('ORDINA([I, "a"])', CentvrionError), # ORDINA mixed types ("DESIGNA x VT I\nORDINA(x)", CentvrionError), # ORDINA on id (non-array) + ("ORDINA([I, II], V)", CentvrionError), # ORDINA comparator not a function + ("DEFINI bad (a) VT { REDI (VERITAS) }\nORDINA([I, II], bad)", CentvrionError), # ORDINA comparator wrong arity + ("DEFINI bad (a, b) VT { REDI (V) }\nORDINA([I, II], bad)", CentvrionError), # ORDINA comparator returns non-bool + ("ORDINA([I], V, V)", CentvrionError), # ORDINA too many args ("SENATVS(I)", CentvrionError), # SENATVS requires booleans ("SENATVS(VERITAS, I)", CentvrionError), # SENATVS mixed types ("SENATVS([I, II, III])", CentvrionError), # SENATVS array of non-bools