klongpy 0.6.9__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. klongpy/__init__.py +19 -1
  2. klongpy/adverbs.py +5 -5
  3. klongpy/autograd.py +308 -0
  4. klongpy/backend.py +167 -99
  5. klongpy/backends/__init__.py +94 -0
  6. klongpy/backends/base.py +320 -0
  7. klongpy/backends/numpy_backend.py +122 -0
  8. klongpy/backends/torch_backend.py +995 -0
  9. klongpy-0.6.9.data/scripts/kgpy → klongpy/cli.py +65 -88
  10. klongpy/core.py +228 -108
  11. klongpy/db/sys_fn_db.py +4 -3
  12. klongpy/dyads.py +159 -28
  13. klongpy/interpreter.py +31 -3
  14. klongpy/monads.py +39 -3
  15. klongpy/repl.py +21 -3
  16. klongpy/sys_fn.py +128 -17
  17. klongpy/sys_fn_autograd.py +290 -0
  18. klongpy/sys_fn_ipc.py +18 -6
  19. klongpy/sys_fn_timer.py +13 -3
  20. klongpy/web/sys_fn_web.py +14 -4
  21. klongpy-0.7.0.dist-info/METADATA +493 -0
  22. klongpy-0.7.0.dist-info/RECORD +48 -0
  23. {klongpy-0.6.9.dist-info → klongpy-0.7.0.dist-info}/WHEEL +1 -1
  24. klongpy-0.7.0.dist-info/entry_points.txt +2 -0
  25. {klongpy-0.6.9.dist-info → klongpy-0.7.0.dist-info}/top_level.txt +0 -1
  26. klongpy-0.6.9.dist-info/METADATA +0 -448
  27. klongpy-0.6.9.dist-info/RECORD +0 -77
  28. tests/__init__.py +0 -6
  29. tests/gen_join_over.py +0 -119
  30. tests/gen_py_suite.py +0 -77
  31. tests/gen_test_fn.py +0 -259
  32. tests/perf_async.py +0 -25
  33. tests/perf_avg.py +0 -18
  34. tests/perf_duckdb.py +0 -32
  35. tests/perf_gen.py +0 -38
  36. tests/perf_ipc_overhead.py +0 -34
  37. tests/perf_join.py +0 -53
  38. tests/perf_load.py +0 -17
  39. tests/perf_prog.py +0 -18
  40. tests/perf_serdes.py +0 -52
  41. tests/perf_sys_fn_db.py +0 -263
  42. tests/perf_vector.py +0 -40
  43. tests/test_accel.py +0 -227
  44. tests/test_df_cache.py +0 -85
  45. tests/test_eval_monad_list.py +0 -34
  46. tests/test_examples.py +0 -64
  47. tests/test_extra_suite.py +0 -382
  48. tests/test_file_cache.py +0 -185
  49. tests/test_interop.py +0 -180
  50. tests/test_kg_asarray.py +0 -94
  51. tests/test_kgtests.py +0 -65
  52. tests/test_known_bugs.py +0 -206
  53. tests/test_prog.py +0 -107
  54. tests/test_reshape_strings.py +0 -33
  55. tests/test_suite.py +0 -1480
  56. tests/test_suite_file.py +0 -153
  57. tests/test_sys_fn.py +0 -420
  58. tests/test_sys_fn_db.py +0 -88
  59. tests/test_sys_fn_ipc.py +0 -587
  60. tests/test_sys_fn_timer.py +0 -133
  61. tests/test_sys_fn_web.py +0 -50
  62. tests/test_util.py +0 -233
  63. tests/utils.py +0 -126
  64. {klongpy-0.6.9.dist-info → klongpy-0.7.0.dist-info}/licenses/LICENSE +0 -0
klongpy/core.py CHANGED
@@ -3,14 +3,30 @@ import inspect
3
3
  import weakref
4
4
  from enum import Enum
5
5
  import sys
6
+ import numpy
6
7
 
7
- from .backend import np
8
+ from .backend import np, TorchUnsupportedDtypeError, get_default_backend, to_numpy
8
9
 
9
10
  # python3.11 support
10
11
  if not hasattr(inspect, 'getargspec'):
11
12
  inspect.getargspec = inspect.getfullargspec
12
13
 
13
14
 
15
+ def get_dtype_kind(arr, backend):
16
+ """
17
+ Get the dtype 'kind' character for an array (numpy or torch).
18
+
19
+ Returns:
20
+ 'O' for object dtype
21
+ 'i' for integer types
22
+ 'f' for float types
23
+ 'u' for unsigned integer
24
+ 'b' for boolean
25
+ 'c' for complex
26
+ """
27
+ return backend.get_dtype_kind(arr)
28
+
29
+
14
30
  class KlongException(Exception):
15
31
  pass
16
32
 
@@ -51,11 +67,55 @@ class KGFn:
51
67
 
52
68
 
53
69
  class KGFnWrapper:
54
- def __init__(self, klong, fn):
70
+ """
71
+ Wrapper for KGFn that enables calling from Python with dynamic symbol resolution.
72
+
73
+ When a KGFn is stored and later invoked, this wrapper automatically re-resolves
74
+ the symbol to use the current function definition. This matches k4 behavior and
75
+ provides REPL-friendly semantics where function redefinitions take effect immediately.
76
+
77
+ Example:
78
+ fn = klong['callback'] # Returns KGFnWrapper
79
+ klong('callback::{new implementation}')
80
+ fn(args) # Uses the NEW implementation
81
+ """
82
+
83
+ def __init__(self, klong, fn, sym=None):
55
84
  self.klong = klong
56
85
  self.fn = fn
86
+ # Use provided symbol name (cached) or search for it
87
+ self._sym = sym if sym is not None else self._find_symbol(fn)
88
+
89
+ def _find_symbol(self, fn):
90
+ """Find which symbol this function is currently bound to"""
91
+ if not isinstance(fn, KGFn) or isinstance(fn, KGCall):
92
+ return None
93
+
94
+ # Search the context for this function
95
+ # Skip reserved symbols (x, y, z, .f) which are function parameters, not stored callbacks
96
+ for sym, value in self.klong._context:
97
+ # Skip reserved symbols - use the module constants
98
+ if sym in reserved_fn_symbols or sym == reserved_dot_f_symbol:
99
+ continue
100
+ if value is fn:
101
+ return sym
102
+ return None
57
103
 
58
104
  def __call__(self, *args, **kwargs):
105
+ # Try to resolve dynamically first if we have a symbol
106
+ if self._sym is not None:
107
+ try:
108
+ current = self.klong._context[self._sym]
109
+ if isinstance(current, KGFn) and not isinstance(current, KGCall):
110
+ # Use the current definition
111
+ if len(args) != current.arity:
112
+ raise RuntimeError(f"Klong function called with {len(args)} but expected {current.arity}")
113
+ fn_args = [np.asarray(x) if isinstance(x, list) else x for x in args]
114
+ return self.klong.call(KGCall(current.a, [*fn_args], current.arity))
115
+ except KeyError:
116
+ # Symbol was deleted, fall through to original function
117
+ pass
118
+
59
119
  if len(args) != self.fn.arity:
60
120
  raise RuntimeError(f"Klong function called with {len(args)} but expected {self.fn.arity}")
61
121
  fn_args = [np.asarray(x) if isinstance(x, list) else x for x in args]
@@ -206,16 +266,36 @@ def to_list(a):
206
266
  return a if isinstance(a, list) else a.tolist() if np.isarray(a) else [a]
207
267
 
208
268
 
209
- def is_integer(x):
210
- return issubclass(type(x), (int, np.integer))
269
+ def is_integer(x, backend):
270
+ if issubclass(type(x), (int, numpy.integer)):
271
+ return True
272
+ # Handle 0-dim numpy arrays
273
+ if isinstance(x, numpy.ndarray) and x.ndim == 0:
274
+ return numpy.issubdtype(x.dtype, numpy.integer)
275
+ # Handle backend-specific scalar integers (e.g., torch tensors)
276
+ return backend.is_scalar_integer(x)
211
277
 
212
278
 
213
- def is_float(x):
214
- return issubclass(type(x), (float, np.floating, int))
279
+ def is_float(x, backend):
280
+ if issubclass(type(x), (float, numpy.floating, int)):
281
+ return True
282
+ # Handle 0-dim numpy arrays
283
+ if isinstance(x, numpy.ndarray) and x.ndim == 0:
284
+ return numpy.issubdtype(x.dtype, numpy.floating)
285
+ # Handle backend-specific scalar floats (e.g., torch tensors)
286
+ return backend.is_scalar_float(x)
215
287
 
216
288
 
217
- def is_number(a):
218
- return is_float(a) or is_integer(a)
289
+ def is_number(a, backend):
290
+ if is_float(a, backend) or is_integer(a, backend):
291
+ return True
292
+ # Handle 0-dim numpy arrays
293
+ if isinstance(a, numpy.ndarray) and a.ndim == 0:
294
+ return numpy.issubdtype(a.dtype, numpy.number)
295
+ # Handle 0-dim backend tensors as numbers
296
+ if backend.is_backend_array(a) and hasattr(a, 'ndim') and a.ndim == 0:
297
+ return True
298
+ return False
219
299
 
220
300
 
221
301
  def str_is_float(b):
@@ -232,92 +312,76 @@ def in_map(x, v):
232
312
  return False
233
313
 
234
314
 
235
- def kg_asarray(a):
236
- """
237
- Converts input data into a NumPy array, ensuring all sub-arrays are also NumPy arrays, to meet the requirements of KlongPy.
238
-
239
- KlongPy treats NumPy arrays as data and Python lists as "programs". Therefore, it is crucial that all elements and
240
- sub-arrays of the input data are converted into NumPy arrays. This function attempts to achieve this, while handling
241
- unpredictable and complex data structures that may result from prior manipulations to the data.
242
-
243
- The function first tries to convert the input data into a NumPy array. In the case of a jagged structure
244
- (i.e., irregularly-shaped sub-arrays), it defaults to the object data type. If the initial conversion is unsuccessful,
245
- it tries to convert the input data to an object dtype array. If all these attempts fail, it converts each element to
246
- a list, if it is a NumPy array, or keeps the original element if it is not, and then attempts to convert the whole
247
- structure into an object dtype array again.
248
-
249
- Detecting the type in NumPy directly, as opposed to checking it in Python, significantly enhances
250
- performance, hence the approach adopted in this function.
251
-
252
- Parameters
253
- ----------
254
- a : list or array-like
255
- The input data to be converted into a NumPy array.
256
-
257
- Returns
258
- -------
259
- arr : ndarray
260
- The converted input data as a NumPy array, where all elements and sub-arrays are also NumPy arrays.
261
- """
262
- if isinstance(a, str):
263
- return str_to_chr_arr(a)
264
- try:
265
- arr = np.asarray(a)
266
- if arr.dtype.kind not in ['O','i','f']:
267
- raise ValueError
268
- except (np.VisibleDeprecationWarning, ValueError):
269
- try:
270
- arr = np.asarray(a, dtype=object)
271
- except ValueError:
272
- arr = [x.tolist() if np.isarray(x) else x for x in a]
273
- arr = np.asarray(arr, dtype=object)
274
- arr = np.asarray([kg_asarray(x) if isinstance(x, list) else x for x in arr], dtype=object)
275
- return arr
315
+ def kg_asarray(a, backend):
316
+ """Convert input to array using the backend's kg_asarray method."""
317
+ return backend.kg_asarray(a)
276
318
 
277
319
 
278
- def kg_equal(a, b):
279
- """
280
- Compares two values or arrays (including nested arrays) for equality.
281
-
282
- This function recursively checks if two values or arrays are equal. It can handle
283
- nested arrays and is more general-purpose than standard NumPy functions such as
284
- np.array_equal.
285
-
286
- If the inputs are lists, the function checks that their lengths are equal, and
287
- then compares each element pair for equality. If the inputs are NumPy arrays with
288
- the same dtype (excluding object dtype), it uses the np.array_equal function for
289
- comparison.
290
-
291
- For non-list inputs, the function compares the two values directly. If they are
292
- both numbers, it uses np.isclose to allow for minor floating-point differences.
293
-
294
- Parameters
295
- ----------
296
- a, b : Any
297
- The two inputs to compare. These can be any type of values or arrays.
298
-
299
- Returns
300
- -------
301
- bool
302
- True if the two inputs are equal, False otherwise.
303
- """
320
+ def kg_equal(a, b, backend):
321
+ """Compare two values or arrays for equality, handling nested arrays and tensors."""
304
322
  if a is b:
305
323
  return True
306
324
 
307
- na, nb = isinstance(a,np.ndarray), isinstance(b,np.ndarray)
325
+ # Check for arrays (numpy or backend-specific)
326
+ is_numpy_a = isinstance(a, numpy.ndarray)
327
+ is_numpy_b = isinstance(b, numpy.ndarray)
328
+ is_backend_a = backend.is_backend_array(a)
329
+ is_backend_b = backend.is_backend_array(b)
308
330
 
309
- if na and nb and a.dtype == b.dtype and a.dtype != 'O':
310
- return np.array_equal(a,b)
331
+ na, nb = is_numpy_a or is_backend_a, is_numpy_b or is_backend_b
311
332
 
312
- na, nb = na or isinstance(a,list), nb or isinstance(b,list)
333
+ # Handle arrays with same dtype
334
+ if na and nb:
335
+ a_dtype = get_dtype_kind(a, backend)
336
+ b_dtype = get_dtype_kind(b, backend)
337
+ if a_dtype == b_dtype and a_dtype != 'O':
338
+ return bool(np.array_equal(a, b))
339
+
340
+ na, nb = na or isinstance(a, list), nb or isinstance(b, list)
313
341
 
314
342
  if na != nb:
343
+ # One is array/list, the other is not - could be scalar tensor/array vs scalar
344
+ # Handle comparing 0-dim arrays/tensors with scalars
345
+ if is_numpy_a and a.ndim == 0 and not nb:
346
+ return kg_equal(a.item(), b, backend)
347
+ if is_numpy_b and b.ndim == 0 and not na:
348
+ return kg_equal(a, b.item(), backend)
349
+ if is_backend_a and hasattr(a, 'ndim') and a.ndim == 0 and not nb:
350
+ return kg_equal(backend.scalar_to_python(a), b, backend)
351
+ if is_backend_b and hasattr(b, 'ndim') and b.ndim == 0 and not na:
352
+ return kg_equal(a, backend.scalar_to_python(b), backend)
315
353
  return False
316
354
 
317
355
  if na:
318
- return len(a) == len(b) and all(kg_equal(x, y) for x, y in zip(a, b))
319
-
320
- return np.isclose(a,b) if is_number(a) and is_number(b) else a == b
356
+ # Handle 0-dim arrays/tensors - compare as scalars
357
+ a_is_0d = hasattr(a, 'ndim') and a.ndim == 0
358
+ b_is_0d = hasattr(b, 'ndim') and b.ndim == 0
359
+ if a_is_0d or b_is_0d:
360
+ a_val = backend.scalar_to_python(a) if a_is_0d else a
361
+ b_val = backend.scalar_to_python(b) if b_is_0d else b
362
+ return kg_equal(a_val, b_val, backend)
363
+ return len(a) == len(b) and all(kg_equal(x, y, backend) for x, y in zip(a, b))
364
+
365
+ if is_number(a, backend) and is_number(b, backend):
366
+ # Convert tensors to Python scalars for comparison
367
+ if backend.is_backend_array(a):
368
+ a = backend.scalar_to_python(a)
369
+ if backend.is_backend_array(b):
370
+ b = backend.scalar_to_python(b)
371
+ result = np.isclose(a, b)
372
+ # np.isclose might return an array/tensor, ensure we return bool
373
+ if hasattr(result, 'item'):
374
+ return bool(result.item())
375
+ return bool(result)
376
+
377
+ result = a == b
378
+ # Handle tensor/array result from comparison
379
+ if hasattr(result, 'all'):
380
+ # For arrays, check if all elements are equal
381
+ return bool(result.all())
382
+ if hasattr(result, 'item'):
383
+ return bool(result.item())
384
+ return bool(result)
321
385
 
322
386
  def has_none(a):
323
387
  if isinstance(a,list):
@@ -378,14 +442,19 @@ def rec_flatten(a):
378
442
 
379
443
 
380
444
  def rec_fn(a,f):
381
- return kg_asarray([rec_fn(x, f) for x in a]) if is_list(a) else f(a)
445
+ _backend = get_default_backend()
446
+ return _backend.kg_asarray([rec_fn(x, f) for x in a]) if is_list(a) else f(a)
382
447
 
383
448
 
384
- def vec_fn(a, f):
449
+ def vec_fn(a, f, backend):
385
450
  """
386
451
  Apply a function `f` to an array `a`, with support for both nested arrays and direct vectorized operation.
387
452
  """
388
- return kg_asarray([((vec_fn(x, f)) if is_list(x) else f(x)) for x in a]) if np.isarray(a) and a.dtype == 'O' else f(a)
453
+ if np.isarray(a) and a.dtype == 'O':
454
+ # For object arrays, process each element and preserve structure
455
+ result = [((vec_fn(x, f, backend)) if is_list(x) else f(x)) for x in a]
456
+ return numpy.asarray(result, dtype=object)
457
+ return f(a)
389
458
 
390
459
 
391
460
  def vec_fn2(a, b, f):
@@ -425,18 +494,20 @@ def vec_fn2(a, b, f):
425
494
  not satisfied.
426
495
 
427
496
  """
497
+ _backend = get_default_backend()
498
+ _kg_asarray = _backend.kg_asarray
428
499
  if np.isarray(a):
429
500
  if a.dtype == 'O':
430
501
  if np.isarray(b):
431
502
  assert len(a) == len(b)
432
- return kg_asarray([vec_fn2(x, y, f) for x,y in zip(a,b)])
503
+ return _kg_asarray([vec_fn2(x, y, f) for x,y in zip(a,b)])
433
504
  else:
434
- return kg_asarray([vec_fn2(x, b, f) for x in a])
505
+ return _kg_asarray([vec_fn2(x, b, f) for x in a])
435
506
  elif np.isarray(b) and b.dtype == 'O':
436
507
  assert len(a) == len(b)
437
- return kg_asarray([vec_fn2(x, y, f) for x,y in zip(a,b)])
508
+ return _kg_asarray([vec_fn2(x, y, f) for x,y in zip(a,b)])
438
509
  elif np.isarray(b) and b.dtype == 'O':
439
- return kg_asarray([vec_fn2(a, x, f) for x in b])
510
+ return _kg_asarray([vec_fn2(a, x, f) for x in b])
440
511
  return f(a,b)
441
512
 
442
513
 
@@ -445,7 +516,11 @@ def is_symbolic(c):
445
516
 
446
517
 
447
518
  def is_char(x):
448
- return isinstance(x, KGChar)
519
+ # Check for both core and backend KGChar classes
520
+ if isinstance(x, KGChar):
521
+ return True
522
+ # Also check for backend KGChar (in case they're different classes)
523
+ return type(x).__name__ == 'KGChar' and isinstance(x, str)
449
524
 
450
525
 
451
526
  def is_atom(x):
@@ -457,10 +532,28 @@ def kg_truth(x):
457
532
  return x*1
458
533
 
459
534
 
460
- # TODO: can we just transform chars to ints so that CuPy works?
461
- # we'll need to reassemble the strinsg, so pros/cons.
462
- def str_to_chr_arr(s):
463
- return np.asarray([KGChar(x) for x in s],dtype=object)
535
+ def str_to_chr_arr(s, backend):
536
+ """
537
+ Convert string to character array.
538
+
539
+ Parameters
540
+ ----------
541
+ s : str
542
+ The string to convert.
543
+ backend : BackendProvider
544
+ The backend to use.
545
+
546
+ Returns
547
+ -------
548
+ array
549
+ Array of KGChar objects.
550
+
551
+ Raises
552
+ ------
553
+ UnsupportedDtypeError
554
+ If the backend doesn't support string operations.
555
+ """
556
+ return backend.str_to_char_array(s)
464
557
 
465
558
 
466
559
  def read_num(t, i=0):
@@ -567,6 +660,7 @@ def read_list(t, delim, i=0, module=None, level=1):
567
660
  L := '[' (C|L)* ']'
568
661
 
569
662
  """
663
+ backend = get_default_backend()
570
664
  arr = []
571
665
  i = skip(t,i,ignore_newline=True)
572
666
  while not cmatch(t,i,delim) and i < len(t):
@@ -581,9 +675,24 @@ def read_list(t, delim, i=0, module=None, level=1):
581
675
  if cmatch(t,i,delim):
582
676
  i += 1
583
677
  if level == 1:
584
- aa = kg_asarray(arr)
585
- if aa.dtype.kind not in ['O','i','f']:
586
- aa = np.asarray(arr, dtype=object)
678
+ try:
679
+ aa = kg_asarray(arr, backend)
680
+ if get_dtype_kind(aa, backend) not in ['O','i','f']:
681
+ aa = numpy.asarray(arr, dtype=object)
682
+ except TorchUnsupportedDtypeError:
683
+ # Backend can't handle this data - fall back to numpy object array
684
+ # Recursively convert inner lists to arrays, converting tensors to numpy
685
+ def convert_inner(x):
686
+ if isinstance(x, list):
687
+ try:
688
+ result = kg_asarray(x, backend)
689
+ # Convert tensor to numpy for object array compatibility
690
+ return to_numpy(result)
691
+ except TorchUnsupportedDtypeError:
692
+ return numpy.asarray([convert_inner(e) for e in x], dtype=object)
693
+ # Convert any tensors to numpy
694
+ return to_numpy(x)
695
+ aa = numpy.asarray([convert_inner(x) for x in arr], dtype=object)
587
696
  else:
588
697
  aa = arr
589
698
  return i, aa
@@ -740,7 +849,7 @@ def kg_write_symbol(x, display=False):
740
849
 
741
850
 
742
851
  def kg_write_integer(x, display=False):
743
- return str(x)
852
+ return str(int(x))
744
853
 
745
854
 
746
855
  def kg_write_float(x, display=False):
@@ -763,16 +872,16 @@ def kg_write_string(s, display=False):
763
872
  return ''.join(arr)
764
873
 
765
874
 
766
- def kg_write_dict(d, display=False):
875
+ def kg_write_dict(d, backend, display=False):
767
876
  # determine if the object d has overwritten the default __str__ and call it
768
877
  # if so, otherwise use the default dict str
769
878
  if d.__class__.__name__ != 'dict':
770
879
  return str(d)
771
- return ''.join([':{', ' '.join([kg_write(list(e), display=display) for e in d.items()]), '}'])
880
+ return ''.join([':{', ' '.join([kg_write(list(e), backend, display=display) for e in d.items()]), '}'])
772
881
 
773
882
 
774
- def kg_write_list(x, display=False):
775
- return ''.join(['[', ' '.join([kg_write(q, display=display) for q in x]), ']'])
883
+ def kg_write_list(x, backend, display=False):
884
+ return ''.join(['[', ' '.join([kg_write(q, backend, display=display) for q in x]), ']'])
776
885
 
777
886
 
778
887
  def kg_write_fn(x, display=False):
@@ -785,21 +894,24 @@ def kg_write_channel(x, display=False):
785
894
  return f":outchan.{2 if x.raw == sys.stderr else 1}"
786
895
 
787
896
 
788
- def kg_write(a, display=False):
897
+ def kg_write(a, backend, display=False):
898
+ _backend = backend
899
+ # Convert backend arrays (e.g., torch tensors) to display-friendly format
900
+ a = _backend.to_display(a)
789
901
  if isinstance(a,KGSym):
790
902
  return kg_write_symbol(a, display=display)
791
- elif is_integer(a):
903
+ elif is_integer(a, _backend):
792
904
  return kg_write_integer(a,display=display)
793
- elif is_float(a):
905
+ elif is_float(a, _backend):
794
906
  return kg_write_float(a,display=display)
795
907
  elif isinstance(a,KGChar):
796
908
  return kg_write_char(a,display=display)
797
909
  elif isinstance(a, str):
798
910
  return kg_write_string(a,display=display)
799
911
  elif isinstance(a,dict):
800
- return kg_write_dict(a,display=display)
912
+ return kg_write_dict(a, _backend, display=display)
801
913
  elif is_list(a):
802
- return kg_write_list(a,display=display)
914
+ return kg_write_list(a, _backend, display=display)
803
915
  elif isinstance(a,KGFn):
804
916
  return kg_write_fn(a,display=display)
805
917
  elif isinstance(a,KGChannel):
@@ -810,7 +922,7 @@ def kg_write(a, display=False):
810
922
  return ":undefined"
811
923
 
812
924
 
813
- def kg_argsort(a, descending=False):
925
+ def kg_argsort(a, backend, descending=False):
814
926
  """
815
927
 
816
928
  Return the indices of the sorted array (may be nested) or a string. Duplicate elements are disambiguated by their position in the array.
@@ -826,6 +938,14 @@ def kg_argsort(a, descending=False):
826
938
  """
827
939
  if not is_iterable(a) or len(a) == 0:
828
940
  return a
941
+
942
+ # Fast path: for simple 1D numeric arrays, use native argsort
943
+ if hasattr(a, 'ndim') and a.ndim == 1:
944
+ dtype_kind = get_dtype_kind(a, backend)
945
+ if dtype_kind in ('i', 'f', 'u'):
946
+ return backend.argsort(a, descending=descending)
947
+
948
+ # Slow path: nested arrays or strings need element-by-element comparison
829
949
  def _e(x):
830
950
  return (-np.inf,x) if is_empty(a[x]) else (np.max(a[x]),x) if is_list(a[x]) else (a[x],x)
831
951
  return np.asarray(sorted(range(len(a)), key=_e, reverse=descending))
klongpy/db/sys_fn_db.py CHANGED
@@ -6,6 +6,7 @@ import pandas as pd
6
6
 
7
7
  from klongpy.core import (KGCall, KGLambda, KlongException, reserved_fn_args,
8
8
  reserved_fn_symbol_map)
9
+ from klongpy.backend import np as backend_np
9
10
 
10
11
 
11
12
  class KlongDbException(KlongException):
@@ -179,7 +180,7 @@ def eval_sys_fn_create_table(x):
179
180
  t,"c",,c
180
181
 
181
182
  """
182
- if np.isarray(x):
183
+ if backend_np.isarray(x):
183
184
  return Table({k:v for k,v in x}, columns=[k for k,_ in x])
184
185
  elif isinstance(x, pd.DataFrame):
185
186
  return Table(x)
@@ -202,7 +203,7 @@ def eval_sys_fn_index(x, y):
202
203
  raise KlongDbException(x, "An index may only be created on a table.")
203
204
  if x.has_index():
204
205
  raise KlongDbException(x, "Table already has an index.")
205
- if not np.isarray(y):
206
+ if not backend_np.isarray(y):
206
207
  raise KlongDbException(x, "An index must be a list of column names")
207
208
  for q in y:
208
209
  if q not in x.columns:
@@ -266,7 +267,7 @@ def eval_sys_fn_insert_table(x, y):
266
267
  """
267
268
  if not isinstance(x,Table):
268
269
  raise KlongDbException(x, "Inserts must be applied to a table")
269
- if not np.isarray(y):
270
+ if not backend_np.isarray(y):
270
271
  raise KlongDbException(x, "Values to insert must be a list")
271
272
  batch = len(y.shape) > 1
272
273
  y_cols = len(y[0]) if batch else len(y)