hy icon indicating copy to clipboard operation
hy copied to clipboard

Consider adding Common Lisp's Sharpsign-dot (#.) reader macro

Open gilch opened this issue 8 years ago • 5 comments

Pushing the eval of certain forms back to readtime has various uses. A big one is to swap out code based on various conditions, like the #if and related preprocessor directives from C.

(#.(if (< *version* 2)  old-func  new-func) arg)

This would probably fit well with #740.

Another is to substitute for literals that don't have a literal notation. Compare:

=> (int "1a" 32)  ; base 32 number
int('1a', 32)
42
=> #.(int "1a" 32)  ; expands directly into 42 at readtime, like a literal
42
42
=> (+ "A line that is much too long to fit on one line "
... "doesn't actually have to fit on one line.")
('A line that is much too long to fit on one line ' + "doesn't actually have to fi
t on one line.")
"A line that is much too long to fit on one line doesn't actually have to fit on
 one line."
=> #.(+ "A line that is much too long to fit on one line "
... "doesn't actually have to fit on one line.")
"A line that is much too long to fit on one line doesn't actually have to fit on
 one line."
"A line that is much too long to fit on one line doesn't actually have to fit on
 one line."

Because evaluation happens at read time, it's safe to use in loops and such, without a performance hit for recomputing it every time.

I thought it would be a simple implementation in Hy:

#@((hy.macros.reader ".") (fn [expr] (eval expr)))

And this does seem to work for the above examples, but I'm running into HyModel issues again:

=> #.(lambda [] nil)

[long stack trace including hy.errors.HyCompileError: Internal Compiler Bug \U0001f631\u2937 TypeError: Don't know how to wrap a <class 'function'> object to a HyObject]

But this works fine in Common Lisp:

* #.(lambda () nil)

#<FUNCTION (LAMBDA ()) {1002E222DB}>

Is there a simple change in the macro definition that would fix this? Can we perhaps make everything a HyObject by default if it doesn't already have a model?

gilch avatar Aug 23 '15 00:08 gilch

Is eval-when-compile, which we now have, equivalent to what you asked for here?

Kodiologist avatar Dec 13 '16 17:12 Kodiologist

Maybe. It's not documented, so I'm not sure. And many of my use cases would need a shorter alias.

gilch avatar Dec 14 '16 03:12 gilch

I was wrong; eval-when-compile doesn't expand to anything, so it doesn't do this. It's only for changing the state of the compiler, presumably to affect macro expansions.

Kodiologist avatar Dec 14 '16 05:12 Kodiologist

The Emacs docs on eval-when-compile says it's kind of similar.

Elsewhere, the Common Lisp ‘#.’ reader macro (but not when interpreting) is closer to what eval-when-compile does.

Unlike Hy, in Emacs

The result of evaluation by the compiler becomes a constant which appears in the compiled program.

But since Hy's version doesn't expand to anything, it can't be used that way.

But how can we make a constant appear in the compiled program? As my first attempt demonstrated, only certain data types have a Hy model. To get an arbitrary object into the compiled file, it would have to be serialized somehow. Sounds like a job for pickle.

As a first attempt,

(deftag "." [expr]
  (import pickle)
  `((. (__import__ "pickle")
       loads)
    ~(pickle.dumps (eval expr))))

This does have some runtime overhead. It's not a big deal at the toplevel, but in a nested loop, that would be pretty bad--and that was a major motivation for doing #. in the first place.

I think we can overcome this problem with memoization.

(import pickle)

;; you can memoize with functools in Python3...
(defn loadstatic [bytestr &optional [_memo {}]]
  (try
    (get _memo bytestr)
    (except [KeyError]
      (assoc _memo bytestr (pickle.loads bytestr))
      (loadstatic bytestr))))

(deftag "." [expr]
  `(loadstatic ~(pickle.dumps (eval expr))))

This way it would only get unpickled once. loadstatic could be a core function, or we could have the compiler do it with a special form or a new HyStatic Hy model type or something.

There's two more problems with this though.

First, if you pickle two equal mutable objects, then the memoization will make them point to the same instance. We can avoid this by adding a gensym to the lookup key, e.g. (loadstatic ~(pickle.dumps (, (gensym) (eval expr))))). This gives a different instance per expansion.

And secondly, pickle is also limited in the data types it can serialize. It can't even do lambdas. But there are more capable third-party serializers, like dill. But that's another dependency. Maybe we could try importing that, and fall back to pickle if it's not available.

gilch avatar Sep 20 '17 02:09 gilch

I think you want eval-and-compile, not eval-when-compile.

Kodiologist avatar Sep 20 '17 06:09 Kodiologist

In the linked pull request, I've opted to add do-mac to core in place of the requested reader macro, but for posterity, here's how you could implement it and use in ordinary Hy code under the current reader-macro system:

(defreader .
  (hy.eval (.parse-one-form &reader) (globals)))

#.`(setv ~(hy.models.Symbol (* "x" 5)) 15)
(print xxxxx) ; => 15

Kodiologist avatar Jan 15 '23 16:01 Kodiologist