tomhrr / dale

Lisp-flavoured C
BSD 3-Clause "New" or "Revised" License
1.02k stars 48 forks source link

Anonymous function does not have access to structs defined in outer lexical scope #208

Closed ChengCat closed 5 years ago

ChengCat commented 5 years ago

As the following code shows, anonymous functions does not have access to structs defined in an outer lexical scope.

(import macros)

(using-namespace std.macros
  (def check-struct (macro intern (void)
    (if (exists-type mc (mnfv mc "*Struct*"))
        (printf "*Struct* exists\n")
        (printf "*Struct* does not exist\n"))
    (nullptr DNode))))

(def main (fn extern-c int (void)
  (def *Struct* (struct intern ((a int))))
  (check-struct)
  (def anon-func (var auto \ (fn void (void)
                               (check-struct))))
  0))

I don't completely understand how scoping works with anonymous functions right now, but I think it shouldn't escape from any new-scopes, and it should just introduce a new-scope based on the scope where it's defined. It would still be invalid to reference a local variable from an outer scope, but structs, macros, functions, and possibly others should work fine.

It seems to me that new-scope shouldn't be handled specially, and could be a library macro. namespace would then be the only namespace-related core form.

For some context, I am working on some utilities for closures:

  (def capture (macro extern (rest)
    (local vars (link-nodes-array (arg-count mc) rest))
    (let ((sfields \ (map-nodes vars
                                (qq (uq *node*)
                                  (uq (type-of mc *node* true)))))
          (sconstruct \ (map-nodes vars
                                   (qq (uq *node*)
                                     (uq *node*)))))
      (qq do
        (def *Capture* (struct intern (uq-nc sfields)))
        (local *capture* (cast (# (*Capture* (uq-nc sconstruct)))
                               (p void)))))))

  (def with-captured (macro extern (data rest)
    (local bindings (nullptr DNode))
    (local capstruct (mnfv mc "*Capture*"))
    (or (exists-type mc capstruct)
        (report-error mc data "*Capture* not found"))
    (for-range (i (struct-member-count mc capstruct))
      (local fname (mnfv mc (struct-member-name mc capstruct i)))
      (local binding (qq (uq fname) \ (@:@ (cast (uq data)
                                                 (p (uq capstruct)))
                                           (uq fname))))
      (setf (:@ binding next-node) bindings)
      (setv bindings binding))
    (if (null bindings)
        (qq do (uql-nc (link-nodes-array (- (arg-count mc) 1) rest)))
        (qq let ((uql bindings))
          (uql-nc (link-nodes-array (- (arg-count mc) 1) rest))))))

Then the two macros can be used like this:

(def main (fn extern-c int (void)
  (local x 1)
  (local y 2)

  (capture x y)
  (local func (fn void ((data (p void)))
                (with-captured data
                  (printf "x=%d y=%d\n" x y))))
  (funcall func *capture*)

  1))

Unfortunately this doens't work, because *Capture* can not be referenced from inside the anonymous function.

ChengCat commented 5 years ago

Regarding the two new-scope and namespace forms, after more thoughts, I think the current design is good. namespace is for grouping top-level definitions, and can be reentered at a later point; while new-scope is for introducing a new lexical scope in one of the top-level definitions. They are probably better to be separated as two forms, and it's weird if lexical scopes could be reentered.