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

Interop seamlessly with racket #161

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
172 changes: 155 additions & 17 deletions ac.rkt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#lang racket/base
#lang racket/load
Copy link
Member

Choose a reason for hiding this comment

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

This makes it impossible to provide things which can be required by other Racket modules. This makes it hard to load more than one instance of the Anarki runtime in a Racket program; the only way to load this code more than once is by using something dynamic like eval or dynamic-require. And if that's the approach you think we should take, then you should keep in mind this would currently cause us to have a different ar-atomic-sema semaphore and a different ar-tagged structure type in each Anarki runtime.


; Arc Compiler.


(require

; This defines names like _list, so it would conflict with the
; naming convention for Arc global variables if we didn't prefix it.
(prefix-in ffi: ffi/unsafe)
Expand All @@ -28,10 +26,12 @@

(only-in "brackets.rkt" bracket-readtable)

(for-syntax racket/base))
(for-syntax racket/base)
)

(provide (all-defined-out))
; (provide (all-defined-out))

(read-accept-bar-quote #f)
Copy link
Member

Choose a reason for hiding this comment

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

This side effect potentially clobbers other uses of read in a larger Racket program that happens to load Anarki.


(define-runtime-path ac-rkt-path "ac.rkt")
(define-runtime-path arc-arc-path "arc.arc")
Expand All @@ -41,8 +41,112 @@
(define main-namespace
(namespace-anchor->namespace main-namespace-anchor))

(define lang* (make-parameter 'arc))
Copy link
Member

Choose a reason for hiding this comment

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

Why make this a parameter? Why not pass it as an explicit argument of ac?


(define ac-global-names '(
(do %do)
(cons %cons)
(car %car)
(cdr %cdr)
(caar %caar)
(cadr %cadr)
(cdar %cdar)
(cadar %cadar)
; (_ %_)
(new %new)
(parameterize %parameterize)
(split-at %split-at)
(nand %nand)
(kill-thread %kill-thread)
(break-thread %break-thread)
(thread-send %thread-send)
(thread-receive %thread-receive)
(thread-try-receive %thread-try-receive)
(thread-rewind-receive %thread-rewind-receive)
(write %write)
(require %require)
(normalize-path %normalize-path)
(base64-encode %base64-encode)
(base64-decode %base64-decode)
(print %print)
(uuid %uuid)
(file-size %file-size)
(cddr %cddr)
(list %list)
(and %and)
(or %or)
(assoc %assoc)
(let %let)
(for %for)
(when %when)
(unless %unless)
(+ %add)
(- %sub)
(* %mul)
(/ %divide)
(% %mod)
(= %eql)
(< %lt)
(<= %le)
(>= %ge)
(> %gt)
(empty %empty)
(map %map)
(map1 %map1)
(map2 %map2)
(all %all)
(any %any)
(apply %apply)
(eval %eval)
(uniq %uniq)
(system %system)
(fill-table %fill-table)
(tokens %tokens)
(tag %tag)
(link %link)
(private %private)
(member %member)
(concat %concat)
(compose %compose)
(last %last)
(keep %keep)
(case %case)
(set %set)
(eof %eof)
(string %string)
(read %read)
(max %max)
(min %min)
(abs %abs)
(round %round)
(sort %sort)
(quasiquote %quasiquote)
(time %time)
(date %date)
(count %count)
(nor %nor)
(only %only)
(load %load)
(range %range)
(thread %thread)
(foldl %foldl)
(foldr %foldr)
(partition %parition)
(curry %curry)
(const %const)
(force %force)
(delay %delay)
(read-json %read-json)
(write-json %write-json)
))

(define (ac-global-name s)
(string->symbol (string-append "_" (symbol->string s))))
(if (eqv? (lang*) 'arc)
(let ((x (assoc s ac-global-names)))
(if ;#f
x
(cadr x) (string->symbol (string-append "" (symbol->string s)))))
Copy link
Member

Choose a reason for hiding this comment

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

If this is the way you intended to indent this if call, all right, but I want to call attention to it in case it was an accident.

s))

(define init-steps-reversed* (list))

Expand Down Expand Up @@ -111,14 +215,33 @@
; env is a list of lexically bound variables, which we
; need in order to decide whether set should create a global.

(define (id-literal? x)
(and (symbol? x) (> (string-length (symbol->string x)) 0) (eqv? #\| (string-ref (symbol->string x) 0))))

(define (id-literal x)
(let ((s (substring (symbol->string x) 1 (- (string-length (symbol->string x)) 1))))
Copy link
Member

Choose a reason for hiding this comment

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

This file uses square brackets for let bindings and cond clauses, as per the usual Racket style. If you want to change that, why, and why not change it everywhere else in the file too?

Copy link
Member

Choose a reason for hiding this comment

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

I notice you're shaving off the last character of the identifier here, probably because you're assuming it's a | character, but you never actually check for that; you only check for | at the beginning (in id-literal?).

Why use the syntax |foo| anyway? It looks like you may have been somehow inspired by the existing meaning of |foo|, but the fact this syntax clobbers that one is more of a downside than an upside.

(when (> (string-length s) 0)
(string->symbol s))))

(define (ac-do body env)
(cons 'begin (ac-body* body env)))

(defarc (ac s env)
(cond [(string? s) (ac-string s env)]
[(literal? s) (list 'quote s)]
((keyword? s) s)
((literal? s) (if (eqv? (lang*) 'arc) (list 'quote (ac-quoted s)) s))
((id-literal? s) (id-literal s))
[(ssyntax? s) (ac (expand-ssyntax s) env)]
[(symbol? s) (ac-var-ref s env)]
[(ssyntax? (xcar s)) (ac (cons (expand-ssyntax (car s)) (cdr s)) env)]
((eq? (xcar s) '%id) (cadr s))
((eq? (xcar s) '%arc) (parameterize ((lang* 'arc)) (ac-do (cdr s) env)))
((eq? (xcar s) '%do) (parameterize ((lang* 'arc)) (ac-do (cdr s) env)))
; ((eq? (xcar s) 'begin)(parameterize ((lang* 'rkt)) (ac-do (cadr s) env)))
((eq? (xcar s) '%scm) (parameterize ((lang* 'scm)) (ac-do (cadr s) env)))
((eq? (xcar s) '%rkt) (parameterize ((lang* 'rkt)) (ac-do (cadr s) env)))
[(eq? (xcar s) '$) (ac-$ (cadr s) env)]
[(eq? (xcar s) 'quote) (list 'quote (ac-quoted (cadr s)))]
((eq? (xcar s) 'quote) (if (eqv? (lang*) 'arc) (list 'quote (ac-quoted (cadr s))) s))
((eq? (xcar s) 'lexenv) (ac-lenv (cdr s) env))
[(and (eq? (xcar s) 'quasiquote)
(not (ac-macro? 'quasiquote)))
Expand Down Expand Up @@ -165,6 +288,7 @@

(define (ssyntax? x)
(and (symbol? x)
(eqv? (lang*) 'arc)
(not (or (eqv? x '+) (eqv? x '++) (eqv? x '_)))
(let ([name (symbol->string x)])
(has-ssyntax-char? name (- (string-length name) 2)))))
Expand Down Expand Up @@ -644,18 +768,32 @@
; and it's bound to a function, generate (foo bar) instead of
; (ar-funcall1 foo bar)

(define (stx? expr)
(and (symbol? expr) (eqv? (bound? expr) 'syntax)))

(define (ac-call fn args env)
(if (not (eqv? (lang*) 'arc))
`(,(ac fn env) ,@(map (lambda (x) (ac x env)) args))
(let ([macfn (ac-macro? fn)])
(cond [macfn
(ac-mac-call macfn args env)]
[(stx? fn)
(parameterize ((lang* 'rkt))
`(,(ac fn env) ,@(map (lambda (x) (ac x env)) args)))]
((and (id-literal? fn)
(void? (id-literal fn)))
Copy link
Member

Choose a reason for hiding this comment

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

Checking for void? rarely makes sense. Seems to me the whole point of (void) in Racket is for return values which never vary, which never need to be printed because there's no information that would need to be conveyed. If id-literal is an operation where the return varies in a way we care about, then every time we call (id-literal ...) at a Racket REPL, we should see a result. Since a result of (void) doesn't show up at the REPL, id-literal should never return that.

(map (lambda (x) (ac x env)) args))
Copy link
Member

Choose a reason for hiding this comment

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

This seems incorrect. The point of ac is to compile an Arc expression into a Racket expression. When the thing we're calling is a Racket macro, not all the subforms are necessarily supposed to be expressions; some are patterns or variable bindings or require specifications, etc.

By treating them as expressions, you're changing the meaning of ac so that sometimes it processes things that aren't really expressions. So ac ends up having to be a compiler and an s-expression template DSL at the same time, in different contexts.

I think Anarki's existing approach, using $ to get into Racket and unquote to return to Arc, is already a better s-expression template DSL design than this is.

((or (memf keyword? args)
(id-literal? fn))
`(,(ac fn env) ,@(map (lambda (x) (ac x env)) args)))
[(and (pair? fn) (eqv? (car fn) 'fn))
`(,(ac fn env) ,@(ac-args (cadr fn) args env))]
[(and (ar-bflag 'direct-calls) (symbol? fn) (not (lex? fn env)) (bound? fn)
(procedure? (arc-eval fn)))
(ac-global-call fn args env)]
[#t
`((ar-coerce ,(ac fn env) 'fn)
,@(map (lambda (x) (ac x env)) args))])))
,@(map (lambda (x) (ac x env)) args))]))))
Copy link
Member

Choose a reason for hiding this comment

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

Just so you don't overlook it, I want to point out this unindented layer of parens. Right now it makes it easier to review the diff, but it won't be very nice to have remaining in the code afterward.


(define (ac-mac-call m args env)
(let ([x1 (apply m args)])
Expand All @@ -666,7 +804,7 @@

(define (ac-macro? fn)
(if (symbol? fn)
(let ([v (and (bound? fn) (arc-eval fn))])
(let ([v (and (bound? fn) (not (eqv? (bound? fn) 'syntax)) (arc-eval fn))])
(if (and v
(ar-tagged? v)
(eq? (ar-type v) 'mac))
Expand Down Expand Up @@ -801,11 +939,11 @@
; reduce?

(define (pairwise pred lst)
(cond [(null? lst) ar-t]
[(null? (cdr lst)) ar-t]
(cond [(null? lst) #t]
[(null? (cdr lst)) #t]
[(not (ar-nil? (pred (car lst) (cadr lst))))
(pairwise pred (cdr lst))]
[#t ar-nil]))
[#t #f]))

; not quite right, because behavior of underlying eqv unspecified
; in many cases according to r5rs
Expand Down Expand Up @@ -933,7 +1071,8 @@
((async-channel? x) 'channel)
((evt? x) 'event)
[(keyword? x) 'keyword]
[#t (err "Type: unknown type" x)]))
; [#t (err "Type: unknown type" x)]))
[#t (vector-ref (struct->vector x) 0)]))
Copy link
Member

Choose a reason for hiding this comment

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

Fair implementation for treating Racket as though it's Arc, but for as inconsistent as Arc is about type-related behavior, struct->vector is possibly even more unreliable in Racket. I recently brought up racket/racket#2454 and while they were fixing it, they tweaked several struct->vector results across the language.

I think if you want a catch-all case for exotic Racket values, returning 'unknown would probably be better. Anarki doesn't promise future-compatibility, but it's easy for programmers to overlook that and expect future-compatibility anyway. If a programmer interactively discovers the type of something is 'unknown, I bet that'll raise their awareness of the instability, and they might be able to prepare themselves better for it to become "known" as something else in a future Anarki release.

(xdef type ar-type)

(define (ar-rep x)
Expand Down Expand Up @@ -1603,10 +1742,9 @@ Arc 3.1 documentation: https://arclanguage.github.io/ref.
val))

(define (bound? arcname)
(with-handlers ([exn:fail:syntax? (lambda (e) #t)]
(with-handlers ([exn:fail:syntax? (lambda (e) (if (eqv? arcname '_) #f 'syntax))]
Copy link
Member

Choose a reason for hiding this comment

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

I've had trouble with the _ syntax in Racket interacting with Arc too, lol. :)

I think a more seamless approach to bound? here (which isn't to say a very easy or accessible one) might first involve running all Arc code during a Racket syntax transformer, so that we could call syntax-local-value to determine whether something was bound to Racket syntax. Then, to make the interaction with _ make sense, we would refactor ac so that instead of recursively compiling a whole expression, it would expand to a Racket expression that contained nested occurrences of a Racket macro that expanded its body as Arc code again. That way, when _ is used as a local variable, the Racket macroexpander will understand this, and syntax-local-variable won't look up the module-level meaning anymore.

Relying on Racket's macroexpander like that would railroad the development of the Anarki macro system; I suspect it couldn't do certain things until a new Racket release allowed it to. For all the things Racket's macroexpander can do that ac can't, I think of it as a downgrade in the long term. Racket's macroexpander design is full of dynamic scoping (e.g. the fact syntax-local-value can only be used within the dynamic extent of a macro call) and leaky abstractions (e.g. the fact that Racket macros return s-expressions which their caller can pull apart) and ac is closer to a design path that avoids these things.

[exn:fail:contract:variable? (lambda (e) #f)])
(namespace-variable-value (ac-global-name arcname))
#t))
(namespace-variable-value (ac-global-name arcname))))
Copy link
Member

Choose a reason for hiding this comment

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

If you change bound? to return something other than a Boolean, I think it will conform to Racket conventions better to remove the question mark from the name. If you're trying to keep it the way it is for backwards compatibility, then I recommend making a second procedure and leaving this one as a predicate.


(xdef bound (lambda (x) (tnil (bound? x))))

Expand Down
9 changes: 6 additions & 3 deletions arc.arc
Original file line number Diff line number Diff line change
Expand Up @@ -1031,12 +1031,15 @@ Incompatibility alert: 'for' is different in Anarki from Arc 3.1. For Arc
`(up ,var 0 (- (len ,s) 1)
,@body))

(|require| racket/generator)

(def walk (seq f)
"Calls function 'f' on each element of 'seq'. See also [[map]]."
(loop (l seq)
(when acons.l
(f car.l)
(recur cdr.l))))
(if acons.l
(do (f car.l) (recur cdr.l))
(generator? l) (let x (l) (unless (void? x) (f x) (recur l)))
(sequence? l) (walk (sequence->generator l) f))))
Copy link
Member

Choose a reason for hiding this comment

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

The indentation on this seems inconsistent. The shortest case is split across two lines while the longer ones are crammed onto one. At first glance I thought this was an if with four subforms, when it really has six subforms.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, right, isn't the point of walk that it's a stub for people to defextend? I think if you're going to extend it for generators and sequences, those would be better placed in defextend declarations.


(mac accum (accfn . body)
"Runs 'body' (usually containing a loop) and then returns in order all the
Expand Down
17 changes: 17 additions & 0 deletions arc.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#lang racket/load

(compile-allow-set!-undefined #t)
(compile-enforce-module-constants #f)
(require racket/base)

(load "ac.rkt")
(require 'ac)

(require "brackets.rkt")
;(use-bracket-readtable)

(anarki-init-in-main-namespace-verbose)
; (aload "arc.arc")
(aload "libs.arc")


2 changes: 1 addition & 1 deletion arc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,4 @@ if [[ $REPL == definitely || ( $REPL == maybe && $# -eq 0 ) ]]; then
repl="(tl-with-main-settings)"
fi

$rl racket -t "$arc_dir/boot.rkt" -e "(anarki-init-in-main-namespace-verbose) $load $repl" "$@"
$rl racket -t "$arc_dir/as.scm" #-e "(anarki-init-in-main-namespace-verbose) $load $repl" "$@"
9 changes: 9 additions & 0 deletions as.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#lang racket/load
; racket -f as.scm
; (asv)
; http://localhost:8080

(require "arc.scm")
(tl-with-main-settings)