Arc Forumnew | comments | leaders | submitlogin
arc2c update
10 points by sacado 6042 days ago | 53 comments
I finally did it (even if it took longer than I expected, sorry) : arc2c has been updated. There are many improvements and small corrections, but mainly :

- characters, annotations and strings were added. They can be compared via 'is and 'isnt (string are compared by value, not by address, as in canonical Arc).

- Strings : Unicode is (obviously) fully supported (hand-made !), you can change characters with sref and concatenate strings with +. They are implemented as an array of long (each long is a codepoint). That is quite heavy : each character typically consumes 8 bytes (a kind of UTF-64) ! But knowing length, accessing a substring or changing a character is O(1), which would be impossible with UTF-8. I will probably change the representation to UTf-32, though. When printed, strings are converted to UTF-8.

- closures are now real first-class objects : they can be printed and asked their type.

- a hand-made GC was developed. It is currently a mark-&-sweep one. It is obviously working (at least it does not seem to free used memory) and performs better than Boehm on the tests I ran (the perfs are better because we know how data is organized). Boehm is still used in some places, however, but will eventually be removed.

- 'def is implemented, as a primitive.

- 'len is implemented and works on lists and strings.

- a few primitives work better than before (they handle more types).

I will now work on hash tables.



4 points by sacado 6029 days ago | link

New update : now supports floating point numbers. They can be added, subtracted, multiplied, compared with is, isnt, <, >, <=, >=.

There are a few things incompatible with canonical arc regarding the status of flonums ending with .0. They are regarded as integers by arc (but are still written, e.g., 1.0). In arc2c, any literal written x.0 is translated into x and any generated flonum ending by .0 is considered a flonum. That will probably be fixed soon, but I think it is an almost bug in arc (I guess this will be changed in future versions of arc), so, we'll see.

-----

2 points by almkglor 6029 days ago | link

Okay, looks good.

As an aside: maybe it's better to make the primitives really, really primitive?

Basically we define separate %n+ for adding two fixnums, %f+ for adding two flonums, %fn+ for adding a flonum and a fixnum (in that order) and, %nf+ for adding a fixnum and a flonum (in that order).

Then we can write + in lib-ac.scm.arc as:

  (set +
    (fn rest
      ($+ rest)))
  (set $+
    (fn (args)
      (if args
          (if (%cdr args)
              ($sub+ (%car args) (%cdr args))
              (%car args))
          0)))
  (set $sub+
    (fn (accum rest)
      (if rest
          ((fn (val rest)
             (if (%is (%type accum) 'int)
                 (if (%is (%type val) 'int)
                     ($sub+ (%n+ accum val) rest)
                     (if (%is (%type val) 'num)
                         ($sub+ (%nf+ accum val) rest)
                         ; raise exception
                         ()))
                 (if (%is (%type accum) 'num)
                     (if (%is (%type val) 'int)
                         ($sub+ (%fn+ accum val) rest)
                         (if (%is (%type val) 'num)
                             ($sub+ (%f+ accum val) rest)
                             ; raise exception
                             ()))
                     ; raise exception
                     ())))
            (%car rest) (%cdr rest))
          accum)))
As for macros: Hmm. You see... Look, up in the sky! Is it a bird? A plane? Superman? Nothing? Oh, what were we talking about again? ^^ LOL

Okay now serious: currently I'm blocked with creating a "protected" eval for macros. Programmer's block. The bit blocking me is the destructuring thing. Erk.

-----

1 point by sacado 6029 days ago | link

I think you're right for primitives. I asked pg the same question a few time ago, he answered he didn't know yet. Anyway, for an optimizing compiler, we will need these (as in : "hmmm, n can only hold a fixnum, let's translate this heavy '+ in a lightweight '%n+ !")

Now, macros. I tried to think about it yesterday, and I don't really understand why you need this protected mode. I mean, macros have to be defined (globally) before they are called and treated as such. Since you can already identify all the globals (and since a macro is a global thing annotated by 'mac), it should be easy to

- make a list of all the symbols holding a macro at a given moment in the code

- explore all the sexprs to see if one of those symbols is the first element of any sublist

- if it is to, make the transformation

- do it again until nothing changes anymore

Am I missing something obvious ?

-----

3 points by almkglor 6028 days ago | link

  (do
    (= xe 0)
    (mac somethingstupid ()
      (= xe (+ xe 1))
      `(prn "something stupid" ,xe)))

... and 'xe is a function in....?

-----

2 points by sacado 6027 days ago | link

OK , so I was missing something obvious :)

If I understand well, we have to interpret the code as we compile it (as a macro's body is executed when a function is defined/compiled, not when executed) ? To me, macros were something translating a piece of code into another one, period. But that's not always the case as your example shows. If someone writes

  (mac foo
    (prn "blah"))

  (def bar (n)
    (foo) (* n 2))
That means we have to display "blah" on compile time, right ? Hmm, that's much harder than I thought... It really means implementing an arc interpreter in arc (the way the official one is written in mzscheme), with for example prepending all the interpreted names with _ to distinguish them from the compiler's names...

Ok, I think I'm starting to realy get it ; or am I still missing an even trickier issue ?

-----

2 points by almkglor 6027 days ago | link

That is about it. ^^ However my current implementation uses tables as environments for the function. Keys in the table are symbols for the variable names in that environment. A "parent" string key accesses the environment's parent.

The global environment is shadowed by a table; when this table is accessed, if a table entry does not exist, we look it up in the real environment using 'eval (!!) and retain a copy. This allows the macro to use stuff like 'map.

However there is still a leak here: if the real global-level environment has a function that modifies a global, it will modify a different global variable from the one that is visible to the macro. This shouldn't happen anyway: we should really avoid using globals in utility functions ^^.

-----

2 points by almkglor 6026 days ago | link

Okay, now that I've actually gotten to see it - I think you forgot to push codegen.arc, since the current version on git don't actually generate code that calls DBL2OBJ.

Edit: Also, it might be useful to organize the primitives as "functions", like what I did with cons_fun().

So:

  #define CONS() {obj d = POP(); TOS() = cons_fun(TOS(), d);}
This should allow us to express some primitive calls directly:

  (%cons foo (%car bar))
  ==>
  PUSH(cons_fun(foo, car_fun(bar)));
Also, it might be useful to merge 'aquote and 'alit structures into a single structure.

-----

1 point by sacado 6025 days ago | link

Oops, thank you :/ I just pushed codegen.

I'll work on what you said about primitive functions in a few days (Monday or Tuesday I guess).

-----

2 points by almkglor 6020 days ago | link

Err, no. I just realized while on vacation that it's not safe to use memory-allocating primitives as functions. The problem is with nested conses:

  QUOTE_CONSTANT[0] = cons_fun(FIX2OBJ(1),cons_fun(FIX2OBJ(2),NILOBJ))
Let's call the cons_fun(FIX2OBJ(1) ...) call cons_fun1, and the other as cons_fun2. cons_fun2 is called first (because it's more inner). The value is then pushed into the C stack. Then cons_fun1 is called. However, what if GC is triggered at exactly cons_fun1? Then the cons cell returned by cons_fun2 is on the C stack, not the Arc stack; the C stack is not scanned by the GC! This means we actually lose memory areas.

Probably means I have to modify the way constants are stored.

-----

1 point by sacado 6020 days ago | link

Hmmm, good point. The stack, GLOBALS and QUOTE_CONSTANTS are all explored when GC is triggered. Maybe the solution would be to add a temporary storage (that should be taken into consideration by GC too) before putting things in the constants, or something like that ?

-----

1 point by almkglor 6020 days ago | link

> Maybe the solution would be to add a temporary storage (that should be taken into consideration by GC too) before putting things in the constants, or something like that ?

Called the "Arc stack", maybe? ^^

Currently this is what is done:

  QUOTE_CONSTANTS[0] = cons_fun(FIX2OBJ(1),cons_fun(FIX2OBJ(2),NILOBJ))
It should probably be:

  PUSH(FIX2OBJ(1));
  PUSH(FIX2OBJ(2));
  PUSH(NILOBJ);
  CONS();
  CONS();
  QUOTE_CONSTANTS[0] = POP();
Of course, the thing about having _fun functions is, they are safe for non-allocating functions. So we could safely directly translate:

  (car (cdr foo))
to:

  PUSH(car_fun(cdr_fun(LOCAL(3)/*foo@42*/)));
Edit: of course, even though it's named "_fun" doesn't mean that it is a function:

  #define car_fun(o) (((pair *)(o))->car)

-----

2 points by almkglor 6020 days ago | link

Okay, converted init_constants() to use the Arc stack instead of the C stack. Fear the pointer arithmetic foo-ness when using a processor with special PUSH and POP instructions (which are arguably dying out, because RISC processors handle stacks using explicit pointer ariths).

-----

3 points by sacado 6037 days ago | link

I've got a dumb (but obviously working) implementation of hash tables. Actually, it is so dumb that it cannot really be called a hash table, but at least you can use it.

The implementation consists in two arrays : one for the keys, the other one for values. When you look for an object, it goes through the keys array and performs 'is comparison until it finds it. It is as scalable as alists, I know, but I'll work on the actual implementation later...

There are a few other new things. Most notably, the functional notation for accessing parts of lists/strings/hash tables is now implemented : ('(foo bar baz) 2) returns 'baz.

I thought it would bring horrible performance : since something at a functional position can be not only a closure, but also a string, a table or a pair, you have to do multiple dispatch every time you call a closure. Ugh... Well, obviously, gcc (with -O3) is very clever, or I'm missing something, because the actual slowdown isn't really significative.

Will be pushed tonight (Paris time).

-----

3 points by almkglor 6037 days ago | link

Note that a list key is compared using 'iso, not 'is (the quoted list bug notwithstanding)

> There are a few other new things. Most notably, the functional notation for accessing parts of lists/strings/hash tables is now implemented : ('(foo bar baz) 2) returns 'baz.

Cool! How'd you implement it?

I assume it doesn't have support for the 'call* table yet?

-----

3 points by sacado 6036 days ago | link

It looks a little hackish. I implemented it in the END_JUMP macro. If the object in LOCAL(0) (supposed to be a closure) is a string, a table or a cons, perform the adequate lookup operation (taking LOCAL(2) as a parameter). Then perform the actual jump : BEGIN_JUMP, PUSH(LOCAL(1) (which is the continuation)), PUSH(resut of lookup operation), END_JUMP.

call* is not implemented yet.

As for keys in hash tables, I admit I didn't really bother yet. It is quite broken in canonical Arc anyway (and in Anarki too for that matter) : it doesn't work correctly with lists and does not work correctly with strings too (if you update a string key, you're dead). If the hash itself is its own key, it breaks, but I don't know if this is supposed to be feasible or not.

So I'll focus on symbols and numbers as keys, first, as they are the most usual key types anyway, and will gradually introduce other key types.

-----

3 points by almkglor 6036 days ago | link

Just wondering if it's possible to create a bunch of primitives to do the referencing?

  %string-ref
  %list-ref
  %table-ref
Then possibly we could add to lib-ac.scm.arc:

  (= call* (%table)) ;or whatever the table-creating primitive is
  (%sref-table call* 'string
    (fn (s i) (%string-ref s i)))
  (%sref-table call* 'list
    (fn (s i) (%list-ref s i)))
  (%sref-table call* 'table
    (fn (s i) (%table-ref s i)))
  ; for compatibility with existing Anarki
  (set ref
    (fn (c i)
      (if (%is (%type c) 'string)
          (%string-ref c i)
          (if (%is (%type c) 'list)
              (%list-ref c i)
              (if (%is (%type c) 'table)
                  (%table-ref c i)
                  ; dummy stub for error message, when
                  ; errors are implemented
                  ())))))
Then maybe change code-execute* to something like:

  obj fn = SYM2OBJ("fn");
  obj typ;
  goto initjump;
  jump:
  // most common case, so check it first
  if((typ = type(LOCAL(0))) == fn){
    goto realjump;
  } else {
    memmove(&LOCAL(3),&LOCAL(2), (num_args - 2)*sizeof(obj));
    ++num_args;
    LOCAL(2) = LOCAL(0);
    LOCAL(0) = table_lookup(GLOBAL(CALL_TABLE), typ);
    //possibly add a check here to see if it's a function
  }
  realjump:
  pc = LOCAL(0)[0]; //or whichever it should be
  initjump:
  switch(pc){
    ...

-----

1 point by sacado 6036 days ago | link

good idea, I'll implement that soon...

-----

1 point by almkglor 6036 days ago | link

  -#define CAR() { if (TOS() != NILOBJ) {pair * p = (pair *) POP(); PUSH((obj)(p->car)); }}
  -#define CDR() { if (TOS() != NILOBJ) {pair * p = (pair *) POP(); PUSH((obj)(p->cdr)); }}
  +#define CAR() { pair * p = (pair *) POP(); PUSH((obj)(p->car)); }
  +#define CDR() { pair * p = (pair *) POP(); PUSH((obj)(p->cdr)); }
Just wondering: Why was this changed? Shouldn't it be that (car nil) == nil?

-----

1 point by sacado 6036 days ago | link

oh, oh, that's a mistake, sorry. I forgot to merge this with my own code. I'll do it on the next commit, if you don't do it before me...

-----

1 point by almkglor 6036 days ago | link

I'll change it back then ^^

Edit: done

-----

1 point by sacado 6037 days ago | link

pushed

-----

3 points by kens 6042 days ago | link

I gave arc2c a try, and it segfaults in END_JUMP with a -1 index when I try running an executable.

Am I using the right version? There's arc/arc2c in Anarki and there's arc2c off github.com/sacado/arc2c. Are they the same?

-----

1 point by sacado 6042 days ago | link

Except if someone pushed it soon, arc2 on Anarki is not the current version. The up-to-date version is sacado/arc2c.

-----

3 points by kens 6041 days ago | link

How do you organize your directories for arc2c? In particular, where does Anarki go relative to arc2c? Do you overlap the git repositories? And then what directory do you start arc in? (I can't get the paths to work for the loads.)

-----

1 point by sacado 6041 days ago | link

I'm not sure I fully understand your question, but you are supposed to put all the arc2c files at the root level, where arc.sh lives, then load arc2c.sh and finally call (compile-file "foo.arc"). Tell me if I didn't answer your question or if you still get something wrong.

-----

2 points by almkglor 6040 days ago | link

minor correction: s/arc2c\.sh/arc2c\.arc/

To be specific:

  $ ls
  arc2c/ arc-wiki/
  $ cd arc-wiki/
  $ cp -r ../arc2c/* .
  $ ./arc.sh
  Use (quit) to quit, (tl) to return here after an interrupt.
  arc> (load "arc2c.arc")
  nil
  arc> (compile-file "t.arc")
   <lots of stuff>

-----

2 points by kens 6039 days ago | link

Yes, that's what I was looking for: where to put arc2c in the tree.

My next arc2c problems are a) gc.h is missing; do I need to download it somewhere? and b) ‘QUOTE_CONSTANTS’ undeclared - I can't figure out where it gets declared. I'm trying to compile simply "(+ 1 1)".

-----

1 point by almkglor 6039 days ago | link

a) Yes, you need the Boehm GC. What I do is, I comment out gc.h and add #define GC_MALLOC malloc #define GC_INIT()

b) That's a bug

-----

2 points by kens 6039 days ago | link

How do I push changes to the git? "git push" gives me "fatal: The remote end hung up unexpectedly". I set up a ssh public key. Do I need to get authorization from you guys? (I've never done a git push before, so assume I may be doing something stupid.)

-----

1 point by sacado 6038 days ago | link

You're on the collaborators' list now. Just push again, and everything should be working.

-----

1 point by stefano 6038 days ago | link

You'll have to ask sacado to add you to the list of committers.

-----

2 points by almkglor 6042 days ago | link

re: GC - I think you're not properly handling sharedvar's (but then I don't have much time to read it). Specifically I think you're not properly marking sharedvar's. Since your GC needs to know the structure, you might need to put type tags on sharedvar's after all.

re: primitives: I think it's better to split the primitives: %+ for numeric addition, %string+ for string addition. Then define in lib-ac.scm.arc:

  (set +
    (fn (a b)
      (if (and (is (type a) 'int) (is (type b) 'int))
          (%+ a b)
          (if (and (is (type a) 'string) (is (type b) 'string))
              (%string+ a b)
              (%err "+: type mismatch")))))
We can even define the above as $+ and define + as variadic, reducing the input using $+, as in canonical Arc.

-----

1 point by sacado 6041 days ago | link

Yep, I did not try to handle sharedvars at all with my GC. I will eventuelly need to give them a type tag, but I deferred that to the next release :) Splitting polymorphic functions is something that has to be done too.

As for variadic +, I don't know : if we transform (+ a b c d) in (+ a (+ b (+ c d))) during the first phase of compilation, we obviously handle variadic +, but at the end (in the generated code), the ADD() primitives only handles + with 2 args, which I guess would be more efficient ? And, that way, we could translate (+ a 1 b 2) into (+ a (+ 3 b)), and (+) to 0, which will eventually have to be done.

-----

3 points by almkglor 6041 days ago | link

Then someone redefines '+ so that it doesn't just add numbers or concatenate strings. Say (+ 1 2 "asdf") becomes "3asdf".

Further, what if we need to pass '+ as a function to another function?

  (def hmm (op)
    (op 1 2 3 4 5))

-----

1 point by sacado 6041 days ago | link

Oh, yes, so that wasn't really a good idea...

-----

2 points by almkglor 6036 days ago | link

re GC: this looks interesting: http://use.perl.org/~chromatic/journal/36212?from=rss

Personally I think memory should be managed by refcounts, and GC only when the cyclic garbage adds up. However adding refcounts is somewhat harder since every state-mutating 'set, 'sref, 'scar, 'scdr, and 'cons needs to decrement the current obj refcount and increment the new obj refcount.

I also suppose that currently the time taken by GC isn't actually very big yet, since all we've been compiling are a few dorky simply bits of Arc code.

-----

3 points by kens 6035 days ago | link

I thought the big problem with refcounting was circular data structures. (Arc supports those, but I haven't seen anything actually use them.)

Which brings to mind the Lisp koan (http://www.lisp.org/humor/ai-koans.html):

One day a student came to Moon and said, "I understand how to make a better garbage collector. We must keep a reference count of the pointers to each cons." Moon patiently told the student the following story-

    "One day a student came to Moon and said, "I understand how to make a better garbage collector...

-----

1 point by almkglor 6035 days ago | link

Which is why there's a backup GC. The point however is that circular data structures are pretty rare anyway. Always optimize for the common case.

-----

1 point by sacado 6035 days ago | link

hmm, interesting. I'm not really fond of refcounting. It makes FFI or C extensions really hard. That's what I don't like with Python's FFI. You always have to think : "do I have to increment the refcount ? decrement it ? Leace it alone ?" If you don't do it well, you have random bugs. The sad story is that Python makes C programming harder than it already is.

On the opposite, palying with mzscheme's or Lua's FFI is a real pleasure : you don't have to bother with GC. You even have (sometimes) your malloced object collected for you.

But if we can cetnralize the refcount operations in a single (or a very small number) of places, I'm OK... Their talking about stack_push / stack_pop is rather inspiring...

For information : On a GC-relativey-intensive program (mainly calculating (fib 40), which generates a lot of garbage), and with a heap of 50 million possible references, for a total time of 228000 ms, i got the following GC info :

  total time : 177ms, nb cycles : 17, max time : 42ms, avg time : 10 ms
That's far from perfect, of course, but it doesn't look so bad to me.

Btw, doctstrings are a real performance killer : they are useless, but are allocated on the heap and fill it up really quick (ah, recursive functions...). We should add something in the code removing immediate values in functions.

-----

3 points by almkglor 6035 days ago | link

> Btw, doctstrings are a real performance killer : they are useless, but are allocated on the heap and fill it up really quick (ah, recursive functions...). We should add something in the code removing immediate values in functions.

Really? You've tried it? Because docstrings are supposed to be removed by the unused global removal step.

Edit: I just did. This is a bug.

Edit2: Fixed and on the git.

-----

1 point by stefano 6035 days ago | link

> Btw, doctstrings are a real performance killer : they are useless, but are allocated on the heap and fill it up really quick (ah, recursive functions...

Are you saying that you alloc a docstring at every function call?

-----

1 point by sacado 6035 days ago | link

Well, for the moment, yes. Every object appearing in the program has to be allocated (it's not an optimizing compiler yet). Useless objects are not detected, so every time the compiler sees a string, it generates code to allocate it, and it is freed on the next GC cycle. Every time, you call the function, that code is executed. Well, that's an easy optimisation, so I'll work on it very soon I guess.

-----

1 point by stefano 6035 days ago | link

Yes, it's not difficult. You just have to find all constant values, create a global var for each constant, assign the const value to the global var and substitute the occurence of the constant with the global var name.

-----

1 point by binx 6035 days ago | link

Refcounting performs a lot worse than a generational gc. When dealing with many deep data structures, it becomes more worse. And a simple generational gc is not very hard to implement.

-----

2 points by almkglor 6035 days ago | link

Well, then: how about a limited form of refcounting, solely for closures used as continuations?

We could even leave off true refcounting, instead just setting a flag on a closure-as-continuation if it's used in 'ccc

-----

2 points by sacado 6040 days ago | link

No hash table working yet but I've started to write a wiki documentation for arc2c on GitHub : http://github.com/sacado/arc2c/wikis/home. Feel free to add things or correct mistakes...

-----

2 points by joshudson 6016 days ago | link

OK, this might be a stupid question.

Is there any hope of getting arc2c able to compile itself anytime soon? This is exactly what you would need for a self-hosting arc environment. -JH

Yes, I'm new here.

-----

1 point by almkglor 6016 days ago | link

None yet. What's currently blocking us badly is handling macros in the code for compilation. Like any decent Lisp program, arc2c makes quite a bit of use of macros.

Unfortunately macros require us to embed an Arc interpreter in the compiler itself, and we can't use the builtin 'eval because we don't want the macros under compilation to accidentally bash global functions/variables used by the compiler itself.

You'll have to contact sacado if you're interested in chipping in, we haven't figured out how to make a git on github publicly pushable.

-----

1 point by sacado 6015 days ago | link

As almkglor said, macros are the real problem as for now. Once we will have them working, a meta-circuar arc2c should be straightforward.

We will need 'read and 'eval functions (that should be easy, and they will probably have to be fully operational for macros anyway) and error handling (a trivial error handling, sufficient for arc2c's code, could be easily implemented : just write an error message and exit). A poor, but meta-circular compiler. Then, we'll have to optimize / improve everything left.

-----

2 points by almkglor 6015 days ago | link

Re: eval - We need a way to implement 'apply.

It's not easy. We can't make it into a primitive, because CPS conversion assumes that primitives will never need access to continuations, and thus does not transform primitive calls to CPS form.

-----

1 point by sacado 6014 days ago | link

Couldn't it be implemented as :

- PUSH the closure to be called

- PUSH the list of args

- call the APPLY() function, which just POPs the two elements, PUSHes all the elements of the arg list one after one, then call the closure (maybe after changing the continuation argument of that closure by hand, at runtime) ?

That's a runtime behavior, for sure, and probably a few cases should be hard-coded in the generated C file. E.g., (apply + '(1 2)) should be translated to (+ 1 2), then to 3, if nothing bad (redefinition of '+ or 'apply by the user) happened. But in the general case, you can't know.

Anyway, we will eventually have to implement an interpreter in the generated code, to deal with dynamic stuff. Maybe closures should be made available through a hashtable mapping their name(s) to actual code, and not only through a hard-coded array as it is now ?

-----

1 point by almkglor 6014 days ago | link

> Couldn't it be implemented as :

> - PUSH the closure to be called

> - PUSH the list of args

> - call the APPLY() function, which just POPs the two elements, PUSHes all the elements of the arg list one after one, then call the closure (maybe after changing the continuation argument of that closure by hand, at runtime) ?

Which continuation argument do you pass?

Suppose it's like this:

  (%car (%apply foo bar))
Then foo is:

  (set foo
    (fn (a b)
      (ccc
        (fn (k)
          (a k b)))))
Question: How does '%apply get access to the continuation, in order to pass to 'foo, which passes it to 'ccc ?

Remember, calling a function consists of the following steps:

1. Push the closure

2. Push the continuation

3. Push the arguments

The problem is step 2: continuation.

Possibly we need to insert the default 'apply during the same step as inserting the default 'ccc ? Then we could define '%apply as accepting the function, the continuation, and a plain list of arguments.

> Anyway, we will eventually have to implement an interpreter in the generated code, to deal with dynamic stuff. Maybe closures should be made available through a hashtable mapping their name(s) to actual code, and not only through a hard-coded array as it is now ?

s/closure/global variable/, maybe?

I think what we could do is, we add a pointer to an obj in the symbol structure. If the symbol is in the GLOBAL() array, this pointer points to that entry, otherwise to a malloc()'ed location.

-----

1 point by sacado 6014 days ago | link

oh, yes, 'apply & CPS... Well, I'll have a look at that today. I'll see if I can work around it.

-----