Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A first PR for a standard lib #30

Draft
wants to merge 41 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
919235d
preliminary modules in the std lib for bool and safe-arith
dhsorens Nov 17, 2022
6896de5
add square to safe-arith
dhsorens Nov 17, 2022
9ee3f53
add some functions for lists
dhsorens Nov 17, 2022
1c90205
add some comments on safe-arith
dhsorens Nov 17, 2022
d778b11
change _ to -
dhsorens Nov 18, 2022
3e82b23
change list-* functions to *
dhsorens Nov 18, 2022
bf7ee54
remove empty-list and change the order of the args in map
dhsorens Nov 18, 2022
7aad918
implement apply in Lurk
dhsorens Nov 18, 2022
08262cc
remove incorrect implementation of apply
dhsorens Nov 18, 2022
ffbfb3d
streamline bool and add one test
dhsorens Nov 18, 2022
edd9b56
shorten code further for bool
dhsorens Nov 18, 2022
4fffea8
correct and streamline map
dhsorens Nov 18, 2022
0db9e06
an initial, probably wrong version of apply
dhsorens Nov 18, 2022
7e2d391
add logical connectives and, or
dhsorens Nov 18, 2022
2ca2dc9
remove unecessary eq functions
dhsorens Nov 18, 2022
a9ad888
add some placeholder tests and some new functions
dhsorens Nov 18, 2022
8b6daf0
add numbered-list-n function and slight reorg
dhsorens Nov 18, 2022
bda6942
add cadr and cddr
dhsorens Nov 18, 2022
9241d45
add list reverse function
dhsorens Nov 18, 2022
a262a78
add functions for tables (lists of lists)
dhsorens Nov 18, 2022
771cf88
improve semantics of fold (thx Jeff)
dhsorens Nov 18, 2022
926b318
add concat to lists
dhsorens Nov 18, 2022
1478aa2
rename to reverse and nth, add last
dhsorens Nov 18, 2022
59925b5
Remove list
weissjeffm Nov 18, 2022
7e98dfd
Update concat to use reverse
weissjeffm Nov 18, 2022
3249f61
More car/cdr composition aliases
weissjeffm Nov 18, 2022
6da7793
Clean up apply function
weissjeffm Nov 18, 2022
122bbc1
Add functions drop and take
weissjeffm Nov 18, 2022
1374831
Add filter function
weissjeffm Nov 18, 2022
c4b2312
Use kebab-case
weissjeffm Nov 21, 2022
cb58bef
Remove call to neq, which doesn't exist
weissjeffm Nov 21, 2022
e7ad840
Handle corner case in apply1 with empty list
weissjeffm Nov 21, 2022
e9ff2c3
Fixup and add tests for list stdlib
weissjeffm Nov 21, 2022
e996b11
not equal, neq, function
dhsorens Nov 21, 2022
4895eb9
car/cdr composition fixes
weissjeffm Nov 21, 2022
2fab48c
More tests
weissjeffm Nov 21, 2022
37936e3
still incorrect table functions but slightly less incorrect
dhsorens Nov 21, 2022
208f72f
Add logical 'not'
weissjeffm Nov 21, 2022
88b1653
Use more standard and short argument names
weissjeffm Nov 22, 2022
abe766d
add some functions around trees and some around sets.
dhsorens Nov 30, 2022
5aafb28
a definition and some functions for maps. Note only numerical keys ar…
dhsorens Dec 2, 2022
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
2 changes: 2 additions & 0 deletions std-lib/bool/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eval-main :
lurkrs bool-test.lurk
13 changes: 13 additions & 0 deletions std-lib/bool/bool-test.lurk
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
!(:load "bool.lurk")

;; test that (bool_neg nil) evaluates to t
!(:assert (bool_neg nil))
dhsorens marked this conversation as resolved.
Show resolved Hide resolved

;; four test cases for eq_bool
!(:assert (eq_bool t t))
!(:assert (eq_bool nil nil))
!(:assert (bool_neg (eq_bool t nil)))
!(:assert (bool_neg (eq_bool nil t)))

;; test that (bool_neg t) evaluates to nil
!(:assert (eq_bool nil (bool_neg t)))
20 changes: 20 additions & 0 deletions std-lib/bool/bool.lurk
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
;; some boolean operations

;; determine if two bools are equal
(let ((eq_bool (lambda (x y)
(if x
(if y
t
nil)
(if y
nil
t )))))
(current-env))
dhsorens marked this conversation as resolved.
Show resolved Hide resolved

;; negate a boolean
(let ((bool_neg (lambda (x)
(if (eq_bool x nil)
t
(if (eq_bool x t)
nil)))))
(current-env))
dhsorens marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions std-lib/list/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eval-main :
lurkrs list-test.lurk
3 changes: 3 additions & 0 deletions std-lib/list/list-test.lurk
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
!(:load "list.lurk")

;; some tests for the list module
62 changes: 62 additions & 0 deletions std-lib/list/list.lurk
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
;; a list is of the form (cons a (cons b (cons ... (cons nil)...)))
;; some definitions for lists

;; empty_list : the empty list
(let ((empty_list '())) (current-env))
dhsorens marked this conversation as resolved.
Show resolved Hide resolved

;; some functions for lists

;; list : make a list out of a (possibly unevaluated) list quote
(letrec ((list (lambda (q)
(if (car q)
(cons (eval (car q)) (list (cdr q)))
'()))))
(current-env))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIST probably requires a macro or language support to be done right (variable number of arguments).

Explicit use of EVAL is probably not desirable, but if it were, you probably want the current env to be involved.

I'm chalking this up to probably not being a useful function as written.

Copy link

@weissjeffm weissjeffm Nov 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came up with that one, and i had a newer version that uses current-env, so that you can do nested lists.

         (list (lambda (q)
                 (if (car q)
                     (cons (eval (car q) (current-env))
                           (list (cdr q)))
                     nil)))

I don't see any other way to produce a literal list (that's not quoted, but rather requires eval'ing args). I mean, aside from a bunch of consing. Obviously using eval is a terrible hack and yes list is supposed to be varargs. Edit: but i think it's useful until there's a native list.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a clue that the concept involved is off. If we really want a function that takes a list and outputs a list, we can just use the identity function.

Normal Lisp LIST, apart from being variadic, doesn't perform explicit evaluation. It just so happens that the args are evaluated before LIST ever sees them.

For example, here is how SBCL defines LIST:

(defun list (&rest args)
  "Construct and return a list containing the objects ARGS."
  args)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I understand this.

What I really want is lisp's LIST, but I don't think that's possible to define in lurk itself right now. I'm sure you understand why I want it, it's for the same purpose everyone uses LIST in lisp - to make a literal list with expressions in it that I want evaluated first. Without that the only way I can think to make such a list is with repeated consing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I understand. I think one urge is for Lisp's LIST, which can indeed be implemented with macros or when we have support for &REST parameters.

I think you also want something that is definitionally not LIST, but that would accomplish a practical purpose. That something is basically MAP-EVAL of some flavor — which is fine.

I just would not call it LIST, since that's confusing and also will clash with some future implementation of LIST which works as it should.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok yes I see your point, I started out trying to implement LIST but realized it's not possible and then kept the name on something that's not the same. What I ended up with is basically (map eval ...) like you said. I am fine with whatever it is not being in the stdlib but I will need something like this as a utility function at least until there's something better. I am ok with just copying it around. Putting it in the stdlib is probably bad because later it will have no purpose and should go away.

Copy link
Contributor

@porcuquine porcuquine Nov 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not even saying not to have such a function. I'm just saying don't call it LIST. You could call it MAP-EVAL or something.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, if we have map (which we will), writing (map eval lst) is too trivial to be worth creating a new function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be a little more complicated than you want to try to inline by hand. Remember:

  • EVAL isn't a function
  • you need the correct environment

As a test, you want to make sure something like this works:

(let ((a 1))
  (do-the-thing '(a 2 (+ a 2))))

I think for that to work, you probably need to also pass (current-env) to do-the-thing.


;; list_eq : determine if two lists are equal
(letrec ((list_eq (lambda (l1 l2)
(if (car l1)
(if (car l2)
(if (= (car l1) (car l2))
(list_eq (cdr l1) (cdr l2))
nil)
nil)
(if (car l2)
nil
t)))))
(current-env))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this whole file be one big letrec? As it stands, aren't we returning a bunch of different envs, each with one function in it? Maybe I'm misunderstanding what (current-env) does.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also what is the difference between list-eq and eq?

> (eq '(a (b)) '(a (b)))
[3 iterations] => T

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, that's funny, somehow I thought that eq didn't work on bool and on lists, but it works on both! These are removed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re it being one big letrec or several, that's a good question. I don't know what convention is; as I understand it, as written each let and letrec updates the current environment to include that variable binding. I've only bundled related/helper functions into the same let or letrec. @porcuquine thoughts on this stylistically?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe (current-env) doesn't do what I thought.

I guess it is not only returning the env at that point, it's also setting the top level's state to include all the variables from that point. In that case the name is a little misleading, the name sounds like it doesn't mutate any state. I'd call it something like capture-env, or use-env or set-top-level-env etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(current-env) returns the current lexical environment.

Separately, when a .lurk file is processed by !(:load …), the toplevel lexical environment used by the REPL is replaced with the result of evaluating each form encountered.

This has the net effect that sequential forms, each extending the environment, can be used to build up an environment containing all the definitions. This trick allows us to define libraries in different files than use them, and for libraries to depend on other libraries. It also allows more modular definitions within a single file.

This is mostly a hack that won't be so important once the RAM subset is available, but it certainly makes things easier in the current world, which we do want and need to support.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So @porcuquine what's your thought on whether to define the lib in one big letrec or a bunch of different ones? I lean toward just using one (assuming that would work just as well).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably multiple, but logically grouped. I'm not 100% sure, but that's my instinct. That gives us an extra layer of structure and will probably help as intermediate stage toward breaking sections out into their own file/module/whatever. It will also facilitate re-ordering for performance.


;; snoc : appends to the right-hand side of a list (opposite of cons)
(letrec
((snoc_iter (lambda (old_list new_list)
(if (car old_list)
(snoc_iter (cdr old_list) (cons (car old_list) new_list))
new_list)))
(snoc (lambda (x lst) (snoc_iter (snoc_iter lst '()) (cons x nil)))))
(current-env))

;; list_length : determines the length of a list
(letrec
((length_iter (lambda (lst cntr)
(if (car lst)
(length_iter (cdr lst) (+ 1 cntr))
cntr)))
(list_length (lambda (lst) (length_iter lst 0))))
(current-env))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can just be called length. I think that matches common lisp. The same probably goes for most of these other functions - you usually don't need to have list in the name.

Also I think we have automatic currying here, so you can try

(letrec ((length_iter (lambda (cntr lst)
			(if (car lst)
			    (length_iter (+ 1 cntr) (cdr lst))
			    cntr)))
	 (list_length (length_iter 0)))
  (current-env))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you say the same goes for list-eq?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list-eq is just eq. There's no need for such a function at all.


;; list_map : a list map function
(letrec
((map_iter (lambda (lst fun accum)
(if (car lst)
(map_iter (cdr lst) fun (snoc (fun (car lst)) accum))
accum)))
(list_map (lambda (lst fun) (map_iter lst fun (cons nil nil)))))
(current-env))
dhsorens marked this conversation as resolved.
Show resolved Hide resolved

;; list_fold : a fold function over lists
(letrec
((list_fold (lambda (lst op accum)
(if (car lst)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is treating NIL specially and will terminate the computation if a NIL is encountered in LST.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I think I mentioned earlier, NIL checks on the CAR of a list when recursing are usually a sign of confused logic (not an absolute rule, but it holds in this case).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point, I have to revisit a lot of the functions I wrote to correct this logic error 🤔

(list_fold (cdr lst) op (op (car lst) accum))
accum))))
(current-env))
Copy link

@weissjeffm weissjeffm Nov 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here - suggest the op argument goes first so you can do this:

  (letrec 
      ((list_fold (lambda (op accum lst)
          (if (car lst)
              (list_fold op (op (car lst) accum) (cdr lst))
              accum)))
       (sum (list_fold (lambda (x y) (+ x y)) 0)))
    (sum '(1 2 3 4 5)))
=> 15

(by the way I notice + isn't a first class function, so I had to introduce the lambda there, not sure if this is something I should open as a separate lurk issue or if it's intentional.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's how it works. All the built-in operators are hardcoded in the circuit and are not actually 'functions'. We could define functions bound to the symbols in a library in the future, as a convenience — or not. Depending on how details of future versions evolve, along with the circuit, it's possible we revisit this, but for now it's how things are by design.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good to know, I had the same question about +

2 changes: 2 additions & 0 deletions std-lib/safe-arith/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eval-main :
lurkrs safe-arith-test.lurk
28 changes: 28 additions & 0 deletions std-lib/safe-arith/safe-arith-test.lurk
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
!(:load "safe-arith.lurk")

;; tests for neg
!(:assert-eq 0 (neg 0))
!(:assert-eq 10 (neg (- 0 10)))
!(:assert-eq (- 0 10) (neg 10))

;; tests for safe_add
!(:assert-eq (+ 10 (neg 43)) (safe_add 10 (neg 43)))
!(:assert-eq (+ (neg 111) 321) (safe_add (neg 111) 321))
!(:assert-eq (+ 11 5443225) (safe_add 11 5443225))
!(:assert-eq (+ (neg 5443225) (neg 43)) (safe_add (neg 5443225) (neg 43)))

;; tests for safe_sub



;; tests for safe_mult



;; tests for floor_int_div



;; tests for ceil_int_div


113 changes: 113 additions & 0 deletions std-lib/safe-arith/safe-arith.lurk
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
;; Arithmetic operations that take into account that arithmetic in Lurk
;; is done over a finite field Z mod p.
;; These functions model arithmetic in Z.

;; Since Lurk uses a finite field for arithmetic, arithmetic doesn't
;; always function as we might expect it to
;; In particular, it is possible for the product or the sum of two
;; positive integers to be negative

;; here we implement "safe" integer arithmetic, which is:
;; integer arithmetic which evaluates correctly when it behaves like
;; normal integer arithmetic, but evaluates to `nil` when it
;; behaves oddly due to us using a finite field

;; a simple function to switch from negative to positive and visa versa
(let ((neg (lambda (n) (- 0 n)))) (current-env))

;; safe_add and safe_subtract make addition behave as in the integers
(let ((safe_add (lambda (a b)
(if (< a 0)
(if (< b 0)
;; the sum of two negatives should be negative
(if (< (+ a b) 0)
;; the sum of a positive and negative is safe
(+ a b))
(+ a b))
(if (< b 0)
;; the sum of a positive and negative is safe
(+ a b)
;; the sum of two positives should be positive
(if (>= (+ a b) 0)
(+ a b)))))))
(current-env))

(let ((safe_sub (lambda (a b)
(if (< a 0)
(if (< b 0)
;; the difference of two negatives is safe
(- a b)
;; the difference of `a` negative and `b` nonnegative
;; should be negative
(if (< (- a b) 0)
(- a b)))
(if (< b 0)
;; the difference of `a` nonnegative and `b` negative
;; should be nonnegative
(if (>= (- a b) 0)
(- a b))
;; the difference of `a` nonnegative and `b` nonnegative is safe
(- a b))))))
(current-env))

;; safe_mult makes multiplication behave as in the integers
(let ((safe_mult (lambda (a b)
(if (< a 0)
(if (< b 0)
;; the product of two negatives is positive
(if (> ( * a b) 0)
( * a b))
;; the product of a negative and nonnegative is nonpositive
(if (<= ( * a b) 0)
( * a b)))
(if (< b 0)
;; the product of a nonnegative and a negative is nonpositive
(if (<= ( * a b) 0)
( * a b))
;; the product of two nonnegatives is nonnegative
(if (>= ( * a b) 0)
( * a b)))))))
(current-env))

;; integer division, where division by 0 results in 0
;; first an auxiliary function `quot`
(letrec
((quot_iter (lambda (a b q)
(if (< a b)
(quot_iter b a 0)
(if (< (* b (+ q 1)) 0)
q
(if (> (* b (+ q 1)) a)
q
(quot_iter a b (+ q 1)))))))
(quot (lambda (a b)
(if (< a 0)
(quot (- 0 a) b)
(if (< b 0)
(quot a (- 0 b))
(quot_iter a b 0))))))
(current-env))

;; integer division that rounds down
(letrec ((floor_int_div
(lambda (a b)
(if (= b 0) 0
(if (< a 0) (- 0 (floor_int_div (- 0 a) b))
(if (< b 0) (- 0 (floor_int_div a (- 0 b)))
(if (< a b) 0
(quot a b))))))))
(current-env))

;; integer division that rounds up
(letrec ((ceil_int_div
(lambda (a b)
(if (= b 0) 0
(if (< a 0) (- 0 (ceil_int_div (- 0 a) b))
(if (< b 0) (- 0 (ceil_int_div a (- 0 b)))
(if (< a b) 1
(+ 1 (quot a b)))))))))
(current-env))


;; safe integer square
(let ((safe_square (lambda (x) (safe_mult x x)))) (current-env))