Skip to content

Commit 06d7861

Browse files
authored
Arrays (Python lists) (#504)
* Arrays without tests * More tests * Last tests * Changelog
1 parent 7533772 commit 06d7861

File tree

6 files changed

+287
-15
lines changed

6 files changed

+287
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
* Added EDN reader in the `basilisp.edn` namespace (#477)
1515
* Added line, column, and file information to reader `SyntaxError`s (#488)
1616
* Added context information to the `CompilerException` string output (#493)
17+
* Added Array (Python list) functions (#504)
1718

1819
### Changed
1920
* Change the default user namespace to `basilisp.user` (#466)

src/basilisp/core.lpy

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,142 @@
14481448
integers."
14491449
short)
14501450

1451+
;;;;;;;;;;;;;;;;;;;;;;;;;;
1452+
;; Arrays (Python List) ;;
1453+
;;;;;;;;;;;;;;;;;;;;;;;;;;
1454+
1455+
(defn to-array
1456+
"Return a Python list with the contents of coll."
1457+
[coll]
1458+
(python/list coll))
1459+
1460+
(defn to-array-2d
1461+
"Return a two-dimensional Python list from the contents of coll.
1462+
1463+
Python lists do not specify a fixed size, so the resulting two-dimensional
1464+
list may be ragged (in the Java sense of the word) if the inner collections
1465+
of the input are ragged."
1466+
[coll]
1467+
(python/list (python/map python/list coll)))
1468+
1469+
(defn into-array
1470+
"Returns a Python list with the values from aseq.
1471+
1472+
The type argument is ignored and is provided only for Clojure compatibility."
1473+
([aseq]
1474+
(python/list aseq))
1475+
([^:no-warn-when-unused type aseq]
1476+
(python/list aseq)))
1477+
1478+
(defn make-array
1479+
"Create a Python list with initial size. If multiple sizes are provided, produces
1480+
a multi-dimensional list-of-lists. There is no efficient way to allocate such
1481+
multi-dimensional lists in Python, so this function will run in polynomial time.
1482+
1483+
Python lists do not support pre-allocation by capacity, so this function pre-
1484+
fills the created list(s) with `nil`.
1485+
1486+
The type argument is ignored and is provided only for Clojure compatibility."
1487+
([size]
1488+
(operator/mul #py [nil] size))
1489+
([^:no-warn-when-unused type size]
1490+
(operator/mul #py [nil] size))
1491+
([^:no-warn-when-unused type size & more-sizes]
1492+
(let [final #py []]
1493+
(loop [size size]
1494+
(if (pos? size)
1495+
(do
1496+
(.append final (apply make-array type more-sizes))
1497+
(recur (dec size)))
1498+
final)))))
1499+
1500+
(defn object-array
1501+
"Create an array of objects.
1502+
1503+
If `init-val-or-seq` and is a `seq` yielding fewer than `size` elements, then the
1504+
remaining indices of the resulting array will be filled with `nil` values.
1505+
1506+
This function does not coerce its argument and is provided for Clojure compatibility."
1507+
([size-or-seq]
1508+
(if (int? size-or-seq)
1509+
(make-array size-or-seq)
1510+
(to-array size-or-seq)))
1511+
([size init-val-or-seq]
1512+
(if (and (seqable? init-val-or-seq) (seq init-val-or-seq))
1513+
(let [final #py []]
1514+
(loop [arr-seq (seq init-val-or-seq)
1515+
idx 0]
1516+
(when (< idx size)
1517+
(.append final (first arr-seq))
1518+
(recur (rest arr-seq) (inc idx))))
1519+
final)
1520+
(operator/mul #py [init-val-or-seq] size))))
1521+
1522+
(defn aclone
1523+
"Return a clone of the Python list."
1524+
[array]
1525+
(python/list array))
1526+
1527+
(defn aget
1528+
"Return the value of the Python list at the index (or indices)."
1529+
([array idx]
1530+
(operator/getitem array idx))
1531+
([array idx & idxs]
1532+
(if (seq idxs)
1533+
(recur (operator/getitem array idx) (first idxs) (rest idxs))
1534+
(operator/getitem array idx))))
1535+
1536+
(defn aset
1537+
"Sets the value of the Python list at the index (or indices). Returns val."
1538+
([array idx val]
1539+
(operator/setitem array idx val)
1540+
val)
1541+
([array idx idx2 & idxs]
1542+
(loop [target (operator/getitem array idx)
1543+
idx idx2
1544+
idx2 (first idxs)
1545+
idxs (rest idxs)]
1546+
(if (seq idxs)
1547+
(recur (operator/getitem target idx) idx2 (first idxs) (rest idxs))
1548+
(do
1549+
(operator/setitem target idx idx2)
1550+
idx2)))))
1551+
1552+
(defn alength
1553+
"Return the length of the Python list."
1554+
[array]
1555+
(python/len array))
1556+
1557+
(defmacro amap
1558+
"Map `expr` over the Python list `array`, returning a new Python list with
1559+
the result.
1560+
1561+
This macro initially binds the symbol named by `ret` to a clone of `array`.
1562+
On each iteration, the index (named by `idx`) of `ret` is set to the return
1563+
value of `expr`."
1564+
[array idx ret expr]
1565+
`(let [len# (alength ~array)
1566+
~ret (aclone ~array)]
1567+
(loop [~idx 0]
1568+
(when (< ~idx len#)
1569+
(aset ~ret ~idx ~expr)
1570+
(recur (inc ~idx))))
1571+
~ret))
1572+
1573+
;; (areduce arr idx ret 0 (+ ret (aget arr idx)))
1574+
(defmacro areduce
1575+
"Reduce the Python list `array` by `expr`, returning the reduced expression.
1576+
1577+
This macro initially binds the symbol named by `ret` to `init`. On each
1578+
iteration, `ret` is rebound to the return value of `expr`."
1579+
[array idx ret init expr]
1580+
`(let [len# (alength ~array)]
1581+
(loop [~ret ~init
1582+
~idx 0]
1583+
(if (< ~idx len#)
1584+
(recur ~expr (inc ~idx))
1585+
~ret))))
1586+
14511587
;;;;;;;;;;;;;;;;
14521588
;; Exceptions ;;
14531589
;;;;;;;;;;;;;;;;

src/basilisp/lang/obj.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ def seq_lrepr(
140140
return f"{start}{seq_lrepr}{end}"
141141

142142

143-
@singledispatch
144143
def lrepr( # pylint: disable=too-many-arguments
145144
o: Any,
146145
human_readable: bool = False,
@@ -192,6 +191,7 @@ def lrepr( # pylint: disable=too-many-arguments
192191
)
193192

194193

194+
@singledispatch
195195
def _lrepr_fallback( # pylint: disable=too-many-arguments
196196
o: Any,
197197
human_readable: bool = False,
@@ -246,17 +246,17 @@ def _lrepr_fallback( # pylint: disable=too-many-arguments
246246
return repr(o)
247247

248248

249-
@lrepr.register(bool)
249+
@_lrepr_fallback.register(bool)
250250
def _lrepr_bool(o: bool, **_) -> str:
251251
return repr(o).lower()
252252

253253

254-
@lrepr.register(type(None))
254+
@_lrepr_fallback.register(type(None))
255255
def _lrepr_nil(_: None, **__) -> str:
256256
return "nil"
257257

258258

259-
@lrepr.register(str)
259+
@_lrepr_fallback.register(str)
260260
def _lrepr_str(
261261
o: str, human_readable: bool = False, print_readably: bool = PRINT_READABLY, **_
262262
) -> str:
@@ -267,53 +267,53 @@ def _lrepr_str(
267267
return f'"{o.encode("unicode_escape").decode("utf-8")}"'
268268

269269

270-
@lrepr.register(list)
270+
@_lrepr_fallback.register(list)
271271
def _lrepr_py_list(o: list, **kwargs) -> str:
272272
return f"#py {seq_lrepr(o, '[', ']', **kwargs)}"
273273

274274

275-
@lrepr.register(dict)
275+
@_lrepr_fallback.register(dict)
276276
def _lrepr_py_dict(o: dict, **kwargs) -> str:
277277
return f"#py {map_lrepr(o.items, '{', '}', **kwargs)}"
278278

279279

280-
@lrepr.register(set)
280+
@_lrepr_fallback.register(set)
281281
def _lrepr_py_set(o: set, **kwargs) -> str:
282282
return f"#py {seq_lrepr(o, '#{', '}', **kwargs)}"
283283

284284

285-
@lrepr.register(tuple)
285+
@_lrepr_fallback.register(tuple)
286286
def _lrepr_py_tuple(o: tuple, **kwargs) -> str:
287287
return f"#py {seq_lrepr(o, '(', ')', **kwargs)}"
288288

289289

290-
@lrepr.register(complex)
290+
@_lrepr_fallback.register(complex)
291291
def _lrepr_complex(o: complex, **_) -> str:
292292
return repr(o).upper()
293293

294294

295-
@lrepr.register(datetime.datetime)
295+
@_lrepr_fallback.register(datetime.datetime)
296296
def _lrepr_datetime(o: datetime.datetime, **_) -> str:
297297
return f'#inst "{o.isoformat()}"'
298298

299299

300-
@lrepr.register(Decimal)
300+
@_lrepr_fallback.register(Decimal)
301301
def _lrepr_decimal(o: Decimal, print_dup: bool = PRINT_DUP, **_) -> str:
302302
if print_dup:
303303
return f"{str(o)}M"
304304
return str(o)
305305

306306

307-
@lrepr.register(Fraction)
307+
@_lrepr_fallback.register(Fraction)
308308
def _lrepr_fraction(o: Fraction, **_) -> str:
309309
return f"{o.numerator}/{o.denominator}"
310310

311311

312-
@lrepr.register(type(re.compile("")))
312+
@_lrepr_fallback.register(type(re.compile("")))
313313
def _lrepr_pattern(o: Pattern, **_) -> str:
314314
return f'#"{o.pattern}"'
315315

316316

317-
@lrepr.register(uuid.UUID)
317+
@_lrepr_fallback.register(uuid.UUID)
318318
def _lrepr_uuid(o: uuid.UUID, **_) -> str:
319319
return f'#uuid "{str(o)}"'

tests/basilisp/core_fns_test.lpy

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,119 @@
334334
(is (thrown? python/IndexError (subs "hello world" 3 12)))
335335
(is (= "lo w" (subs "hello world" 3 7)))
336336
(is (thrown? python/IndexError (subs "hello world" 12 3))))
337+
338+
(deftest to-array-test
339+
(is (= #py [] (to-array [])))
340+
(is (= #py [] (to-array '())))
341+
(is (= #py [1] (to-array [1])))
342+
(is (= #py [1 2 3] (to-array [1 2 3])))
343+
(is (= #py [1] (to-array '(1))))
344+
(is (= #py [1 2 3] (to-array '(1 2 3)))))
345+
346+
(deftest to-array-2d-test
347+
(is (= #py [] (to-array-2d [])))
348+
(is (= #py [] (to-array-2d '())))
349+
350+
(is (= #py [#py [] #py []] (to-array-2d [[] ()])))
351+
(is (= #py [#py [] #py []] (to-array-2d '([] ()))))
352+
353+
(is (= #py [#py [1 2 3] #py [:a :b :c]]
354+
(to-array-2d [[1 2 3] '(:a :b :c)])))
355+
356+
(is (= #py [#py [1 2 3] #py [:a :b :c]]
357+
(to-array-2d '([1 2 3] (:a :b :c)))))
358+
359+
(is (thrown? python/TypeError)
360+
(to-array-2d [[1 2 3] :b])))
361+
362+
(deftest into-array-test
363+
(testing "with no type"
364+
(is (= #py [] (into-array [])))
365+
(is (= #py [] (into-array '())))
366+
(is (= #py [1] (into-array [1])))
367+
(is (= #py [1 2 3] (into-array [1 2 3])))
368+
(is (= #py [1] (into-array '(1))))
369+
(is (= #py [1 2 3] (into-array '(1 2 3)))))
370+
371+
(testing "with (ignored) type argument"
372+
(is (= #py [] (into-array nil [])))
373+
(is (= #py [] (into-array nil '())))
374+
(is (= #py [1] (into-array python/int [1])))
375+
(is (= #py [1 2 3] (into-array python/int [1 2 3])))
376+
(is (= #py [1] (into-array python/float '(1))))
377+
(is (= #py [1 2 3] (into-array python/float '(1 2 3))))))
378+
379+
(deftest make-array-test
380+
(testing "with no type"
381+
(is (= #py [] (make-array 0)))
382+
(is (= #py [nil] (make-array 1)))
383+
(is (= #py [nil nil nil] (make-array 3))))
384+
385+
(testing "with (ignored) type argument"
386+
(is (= #py [] (make-array python/int 0)))
387+
(is (= #py [nil] (make-array python/int 1)))
388+
(is (= #py [nil nil nil] (make-array python/int 3))))
389+
390+
(testing "multi-dimensional"
391+
(is (= #py [#py [] #py []]
392+
(make-array python/int 2 0)))
393+
(is (= #py [#py [nil nil nil] #py [nil nil nil]]
394+
(make-array python/int 2 3)))
395+
(is (= #py [#py [#py [nil nil] #py [nil nil]]
396+
#py [#py [nil nil] #py [nil nil]]]
397+
(make-array python/int 2 2 2)))))
398+
399+
(deftest object-array-tests
400+
(testing "only size"
401+
(is (= #py [] (object-array 0)))
402+
(is (= #py [nil] (object-array 1)))
403+
(is (= #py [nil nil nil] (object-array 3))))
404+
405+
(testing "only seq"
406+
(is (= #py [] (object-array [])))
407+
(is (= #py [] (object-array '())))
408+
(is (= #py [1] (object-array [1])))
409+
(is (= #py [1 2 3] (object-array [1 2 3])))
410+
(is (= #py [1] (object-array '(1))))
411+
(is (= #py [1 2 3] (object-array '(1 2 3)))))
412+
413+
(testing "size and init val"
414+
(is (= #py [] (object-array 0 :a)))
415+
(is (= #py [:a] (object-array 1 :a)))
416+
(is (= #py [:a :a :a] (object-array 3 :a))))
417+
418+
(testing "size and seq"
419+
(is (= #py [] (object-array 0 (range 1 3))))
420+
(is (= #py [1 2 3] (object-array 3 (range 1 4))))
421+
(is (= #py [1 2 3 nil nil] (object-array 5 (range 1 4))))))
422+
423+
(deftest aclone-test
424+
(is (= #py [] (aclone [])))
425+
(is (= #py [] (aclone '())))
426+
(is (= #py [1] (aclone [1])))
427+
(is (= #py [1 2 3] (aclone [1 2 3])))
428+
(is (= #py [1] (aclone '(1))))
429+
(is (= #py [1 2 3] (aclone '(1 2 3)))))
430+
431+
(deftest alength-test
432+
(is (= 0 (alength #py [])))
433+
(is (= 1 (alength #py [1])))
434+
(is (= 3 (alength #py [1 2 3]))))
435+
436+
(deftest aget-test
437+
(is (thrown? python/IndexError (aget #py [] 1)))
438+
(is (thrown? python/IndexError (aget #py [1 2 3] 5)))
439+
(is (thrown? python/IndexError (aget #py [#py[1 0 6] 2 3] 0 5)))
440+
(is (= 1 (aget #py [1 2 3] 0)))
441+
(is (= :d (aget #py [#py [:a :b :c] #py [:d :e :f]] 1 0))))
442+
443+
(deftest aset-test
444+
(let [l #py [1 2 3]]
445+
(is (= :a (aset l 0 :a)))
446+
(is (= #py [:a 2 3] l))
447+
(is (thrown? python/IndexError (aset l 5 :f))))
448+
449+
(let [l #py [#py [:a :b :c] #py [:d :e :f]]]
450+
(is (= 5 (aset l 0 2 5)))
451+
(is (= #py [#py [:a :b 5] #py [:d :e :f]] l))
452+
(is (thrown? python/IndexError (aset l 0 5 :cc)))))

tests/basilisp/core_macros_test.lpy

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,23 @@
279279
(is (= {:meta-kw true :other-kw true}
280280
(meta ^:meta-kw ^:other-kw (fn ([]) ([a] a)))))))
281281

282+
(deftest amap-test
283+
(let [l #py []]
284+
(is (= #py [] (amap l idx ret (+ idx (aget l idx))))))
285+
286+
(let [l #py [1 2 3]]
287+
(is (= #py [1 3 5] (amap l idx ret (+ idx (aget l idx))))))
288+
289+
(let [l #py [1 2 3]]
290+
(is (= #py [6 11 20] (amap l idx ret (apply + ret))))))
291+
292+
(deftest areduce-test
293+
(let [l #py []]
294+
(is (= 0 (areduce l idx ret 0 (+ ret (aget l idx))))))
295+
296+
(let [l #py [1 2 3]]
297+
(is (= 6 (areduce l idx ret 0 (+ ret (aget l idx)))))))
298+
282299
(deftest case-test
283300
(is (= "yes" (case :a :b "nope" :c "mega nope" :a "yes" "nerp")))
284301
(is (= "nope" (case :b :b "nope" :c "mega nope" :a "yes" "nerp")))

0 commit comments

Comments
 (0)