RSS 2.0

Personal Info:

Joe Send mail to the author(s) is a lead architect on an OS incubation project at Microsoft, and was the architect for Parallel Extensions to .NET. He is an author and frequent speaker.

Disclaimer:
The content of this site are my own personal opinions and do not represent my employer's view in anyway.

© 2010, Joe Duffy

 
Searched for : sencha

Prior to coming to Microsoft, I had been a developer for about 6 years.  Back in 2004, when contemplating moving to a different company, I decided I was ready for a shift in focus.  Namely, I wanted to move away from the details of writing code and move towards higher-level architecture, design, and strategy.  So I joined Microsoft as a program manager (PM).

Now, less than 3 years later, I’ve moved back to being a software development engineer (SDE, i.e. developer).  Is it forever?  Who knows?  I didn’t move back because I disliked the PM job (as some might assume).  In fact, I thoroughly enjoyed most aspects of being a PM.  After a few months in my new role I actually find myself missing some of them from time to time.  There wasn’t any single thing that spurred the decision: it was fairly complicated, and involved a mixture of pursuing a specific opportunity, seeking growth, realizing I was missing many things that I enjoyed doing as a developer, and, I suppose, some degree of relapsing back to something I am more comfortable with.

I've written up some details about why I decided to change.  Who knows, maybe it will help somebody out there choose the right role for themselves?

The specific opportunity

The opportunity I’m referring to was, of course, PLINQ, and it came about at just the right time.  I spent the year leading up to my decision gradually focusing more and more on PLINQ.  At first, PLINQ was just a crazy idea.  Then I did some prototyping in my free time, had some quick successes, and was amazed at how simple it was to write parallel programs with LINQ queries.  I showed some people, and most really latched on to the idea; they could understand the value of the system with just a 30 second elevator pitch.  These positive reactions motivated me to stick with the idea, and to keep driving hard.  Then there was interest from the C# team, interest from executives, a BillG ThinkWeek paper, and finally, somebody wanted to do it for real.

In some sense, it was like a start-up.  I had an idea, figured it was an idea worth pursuing, did some prototyping, sold it among my peers, and finally convinced a VC team to fund the execution of the idea.  Well, if it truly were a start-up, what would I then want to do?  Code all night long, obviously!  I wanted to architect and design the actual product version, implement the darned thing, and feel the whole process from start to finish from several perspectives.  Doing PLINQ as a technical lead/developer seemed like a natural next step.

I should also note that I had considered taking developer positions before the PLINQ job.  But in all of those instances, I chose not to for two reasons: I was having a blast as the concurrency PM on the CLR team and because the right opportunity hadn't arisen to steal me away.

This was also a great growth opportunity for me.  Though I was a developer in the past, it was seldom my own idea that I was implementing, and this time it’s different.  I had set the ship sailing in a direction that I was happy with, and experiencing the consequences of that direction is something I seriously wanted.  It’s less about control than it is about feeling more ownership and responsibility for the direction in which our team is headed.  It’s also a way of putting my butt on the line, hopefully winning more trust and respect in the process, showing that I can actually follow through on good ideas (rather than just having the idea itself).  Most PMs certainly get a lot more of the influence and coarse-grained ownership opportunity than most SDEs, but my particular situation was such that my high-level contributions, leadership skills, and domain-experience will be more valuable and influential where I am now.  This last part turned out to be less of a SDE vs. PM thing (i.e. I could have probably had those things as a PLINQ PM too), but it played a huge role in my decision to change teams.

I missed writing code

During the many long nights of prototyping PLINQ, it also kept reminding me how much I loved coding and how much I missed it as a PM.  Put simply: I love programming.  Anybody who’s done something they love and gotten lost in it knows what “flow” is.  I used to experience flow when I played music, but I haven’t done that for so long that programming is now the only time I really feel it.  Maybe it has to do with the fact that I started coding when my brain was still in development (13 years old), and so I tend to be comfortable thinking in code.  You know: no conscious thought, just a direct conduit from your brain to your fingertips.  There is a sense of time standing still, the code simply appearing on the screen in a magical blur, and at some point the thing actually works.  You can get up, stretch, make some tea, map out the next few hours for a moment, and then you’re back into it.  Lost in the fun of it all.  It’s just great, and nothing I ever did as a PM gave me this feeling.

There’s an art to writing code.  Sure, there’s a non-negligible scientific and mathematical component to programming, but I like to think of development as the artful application of those formal techniques.  Inventing, structuring, and reifying abstractions, naming them, the careful placement of comments and whitespace, traversing the state space in your head, using intuition to make certain trade-offs, and so on.

Perhaps the most creative and artistic activity as a PM that I really enjoyed was writing.  But to be honest, I’ve always enjoyed the kind of writing I do on my own time—writing blog essays and books—more than the more prevalent style of writing that a PM tends to do (say, design specifications).  I would get the chance to write a strategic or influential technical document from time to time, but the rate at which I will contribute such things as a SDE seems like it will be the same as when I was a PM.  It has been so far, at least.  If you have the insight and know how, when, and with whom to share and present it, people will listen, regardless of your title.

Designing new things was also of course a creative activity I loved as a PM, but I’m doing more design work as a developer than when I was a PM.  Not all SDEs design higher-level things, but I do regularly and work closely with many architects, distinguished engineers, and technical fellows in the process.  Developers also get to design more at a slightly lower level of abstraction.  But everything's just an abstraction anyway, so the "level" doesn't matter much (to me) anyway.

Prior to PLINQ, my Sencha interpreter/compiler (Scheme at first and later, Common LISP) was my hobby development project.  (And, of course, there were many late nights spent on TopCoder.)  I was basically spending my days as a PM and my nights as a developer.  To be honest, I enjoy doing both things and, aside from the time it took to do both, it made me very happy.  But in retrospect, it turned out to be rather unhealthy.  Writing code was my sole hobby, and I evicted all of my pre-Microsoft hobbies from my system to make room for it.  Now I get to write code during the day, do the components of PM that I enjoyed most, and still preserve my sanity.  I’m just getting back into the things I used to love doing: writing more than just technical blog essays and books, playing guitar, sports, working out, food and wine, and basically just getting outside and feeling healthier, both mentally and physically.  Maybe I'm getting old.

Fractured role structure

There are plenty of things I miss as a PM.  But I associate the absence of many such things with where we are with the PLINQ project.  High-level planning, becoming familiar with the competition and pertinent research in the area, thinking about product strategy, etc. are all certainly crucial activities throughout the project, but they are much more prevalent and involved at the outset.  As the project shifts into execution mode, there’s a lot of thinking about release plans, customer interaction, and team building and management.  I enjoy those things too, but I also love the design, architecture, and implementation of software.  I still get to do them, just not as frequently.  As a developer, you tend not to be involved in as many management discussions and decisions, but I can cope with this knowing I have a solid PM and management team working with me.  I know that if there's anything that requires my attention or where my feedback would be valuable, they won't hesitate to bring it to me.

To be completely honest, I dislike the completely fractured role structure that most of Microsoft employs.  (And “dislike” is putting it mildly.)  I do believe that people need to specialize, particularly as they grow in their career.  You simply can’t do it all and expect to move up to the next level of your career without abstracting some details.  But when you get down to it, there really isn’t much of a difference between PM, SDE, and SDE/T.  I've been doing interviews for PM and SDE lately, and we expect one to have a better business- and customer-sense, while the other needs to be solid algorithmically and implementation-wise, but there's a substantial overlap.  There are obviously certain responsibilities which are unique to each role, but more often than not, the roles are fluid and (when things are going well) people are able to fill in whatever gaps happen to arise that they are good at filling.  I just don’t think this kind of specialization necessarily needs to be forced.  Maybe I'm influenced by my own style of sitting on the edge between the two roles.  Each project needs its own unique structure, and some dedicated managers, but it doesn’t happen according to some magical formula.  As I understand things, this is how it works at Google, and just about every research department I’ve ever known (including MSR).

I should also mention that the grass isn't all a vibrant shade of green on the other side of the fence.  A PM gets to operate at a higher-level, seldom spending more than 10 minutes triaging and talking to a developer about any particular bug, for example.  In contrast, a developer must focus on details most of the time.  This might mean spending anywhere from 1 minute to 1 week on just a single bug.  Some people enjoy getting lost in details (in fact, it can help lead to flow, though bug fixing is more investigative work than anything else, which isn't as conducive).  Checking in a bug makes it really easy to quantify “success” and some people need this.  (Not me.)  I prefer more ambiguous tasks which usually means design or coding an entirely new algorithm, and I find fixing bugs tedious.  This means the late-in-the-game product cycle will be a little rough for me.  But that’s OK: the details are of course part of the engineering process, so I do it and don’t ever find myself thinking “wow, I dislike this” while I'm doing it.  A lot of developers tend to be tools geeks, too.  They love building complex ecosystems of engineering support tools.  I’m not into that.  I value solid engineering practices, but prefer things to be as simple as possible such that nothing gets in between me, the code, and the compiler.  Reality isn’t quite always like this, and tools are often the most effective means to ensure certain quality properties of your product, but I still strive to eliminate (or avoid adding) as much unnecessary obstructions as possible.  A wise man once said "keep it as simple as possible, but no simpler."

Some final words

At the end of the day, when I look at influential people I've bonded with across the company, most (but certainly not all) rose up through the ranks as developers.  That’s clearly very subjective and personal: that’s not to say there aren’t great PMs (past or present), only that most architects, distinguished engineers, technical fellows, and researchers have or are coders at heart.   Moreover, I simply see myself being more successful as a SDE.  Not only have I done it for longer, but the career skills required to move from developer to architect to distinguished engineer (in Microsoft) align more closely with the skills I already have.  While PMs can move to architecture with time, it simply looks like a much longer road from where I stand.

And after saying all of this, I truly believe job title doesn’t matter that much at all.  Ability and good intuition matter.  We’re all just shipping software.  At the end of the day, we do what needs to get done for that to happen, regardless of what hat we happen to be wearing at the time.

3/24/2007 5:19:17 PM (Pacific Daylight Time, UTC-07:00)  #    Comments [3]

Via DBox and TBray, I stumbled upon Will Continuations Continue?, a great essay about why continuation support in modern VMs is not a good idea after all:

"By far he most compelling use case for continuations are continuation-based web servers. ... Rather than relying on the server’s stack to keep track of what location we’re looking at, the [UI] will be a view on a model ... When you pressed “Buy”, it would pass all the information necessary to complete the transaction onto the server. Consequently, we’ll have no more of a pressing need for continuations than traditional applications have today."

I couldn't agree more, although I arrive at the conclusion via a different line of thought.

Just over a year ago now, I was working on continuations for my Scheme interpreter and compiler, Sencha. I managed to create something that "worked" -- in the sense that the stack could be captured, passed around, and restored; and it even still reported locals as roots to the GC -- but there are so many facets of a modern runtime to consider that true product support would be a massive undertaking. I thought continuations were a good idea. Why? To be honest, the main reason was my simple goal of having a full-fidelity Scheme runtime. But I also admired their power.

In retrospect, I now realize something important: the stack is evil. It's a horrible representation of state, especially for web applications.

The stack is unnecessarily bound to an OS thread, and munges control flow with the "state" of the program. The fact that return addresses for function calls lives on it has been the source of many security problems and counter-measures (/GS). When a thread blocks, the entire stack is wasted, even if there is logical work on it that could progress if it weren't for the arbitrary physical association. There's so much crap on it that to summarize the state of your entire program often requires pausing threads and walking their stacks. How dirty and impolite! Freak-of-nature abominations have twisted what the stack was meant for, e.g. COM and GUI reentrancy and APCs, completely disassociating logical and physical representations. You have to reserve a contiguous chunk of the thing per thread (often 1MB), wasting virtual memory space, because Windows doesn't support linked stack regions (not as big a deal on 64-bit as on 32-bit, sure), which also leads to the CLR ripping the process if you ever exceed it (overflow).

So many problems we encounter with parallel programming (among other domains) would go away with a more structured representation of the program as a state machine.

Dharma and the rest of the WF team are delivering just that (in the large). C# 2.0's iterator feature supplies a similar capability (in the small). The Concurrency and Coordination Runtime (CCR) eschews stack in favor of orchestration and message passing. We'll converge at some point. And it won't be around serializing stacks, it will be around getting rid of the damned things.

5/20/2006 12:24:31 PM (Pacific Daylight Time, UTC-07:00)  #    Comments [7]

I drink about 4x more tea than I do coffee. Actually, I wouldn't drink coffee at all if there were decent tea stores in the area with readily available to-go offerings.

I just ordered a bunch of great teas from Upton Tea Imports, including a new 2006 Darjeeling First Flush. I can't wait until they arrive:

  • TD56: Tindharia Estate FTGFOP1 First Flush (EX-1)
  • TS70: Temi Estate FTGFOP1 CL
  • TA93: Nahorhabi Estate FTGFOP1 SPL CL
  • ZO87: Ginseng Tie-Guan-Yin Oolong
  • ZM44: Osmanthus Oolong Se Chung
  • ZW84: Organic Fuding White Treasure
  • ZW90: Organic White Point Reserve
  • ZW99: China White Paklum Tips Reserve
  • TA98: Mothola Estate White Tea
  • TJ77: Spring Harvest Kabusencha

If you're not a tea drinker, or the closest thing to real tea you've had is a soppy Stash tea bag, I encourage you to try one of Upton's sampler sets.

1/21/2006 11:48:26 AM (Pacific Standard Time, UTC-08:00)  #    Comments [2]

Top of the “for fun” stack looks like this:

  • Write, write, write. My book is not moving along as fast as I had hoped. In fact, I'm waaaay behind schedule. Thankfully my publisher's understanding, but I'm certainly testing their patience!
  • Finish reading the two books I picked up recently:
  • Fix bugs in Sencha, especially in the area of quotations. Get more of the SXML, SXSLT, etc. stuff implemented. Work on platform interop. Finish up the standard library functions. VSIP. Interpreted mode? Found some interesting dynamic bugs recently due to the way I statically bind at compile time. Only crops up when working in the toplevel, but (interestingly) this is a pretty big use case for Scheme programming. ;)
  • Get HaskIL off the ground. I've done some work, using GHC Core as the one real truth, but not nearly enough. Need more time. I'm really excited to work on some of the lazy and parallel aspects of this project... gotta get over the hump first.
  • Put my two forks of Rotor back together. I'm learning to navigate the JIT and EE interactions a bit better, although I've come to a particularly unsurprising conclusion: the CLR is a complex beast. I haven't been able to get either of these two things to actually run a program that takes advantage of the changes I've made. Ugh.
    • First class continuations (and coroutines) in the runtime. My approach here is to capture stack state and wrap it up in an object. This can be used to restore at a later point in time. You just call the static method System.Threading.Continuation.Capture(), returning a Continuation instance c; then later you can call c.Restore(). Makes it pretty easy to implement call/cc. I'm now at least a little proficient in writing fcalls; all the whacky macros are making my head hurt. Interactions between stack unwinding and restore is a tricky thing, and I'm finding that CAS is a pickle. My current design does a stack crawl and throws if it notices any type of assert or demand... not for technical reasons, but because it seems pretty bad to capture this and pass it around. Similarly, I fail if you're inside a try { } ... finally { } block since shared state can munged up in a finally (which gets torn down as part of the stack unwind). One of the huge difficulties here is state which doesn't get represented in data structures that the EE knows about--for example, JIT only stack frames. In fact, I'm slowly realizing that this might not be possible without an explicit level of indirection with stack management.
    • Software transactional memory. System.Transactions is one step in the right direction. I'm interested in retry-style STM support, however, directly baked into the runtime. Some of the MSR folks have been doing amazing work here. This one's a tough nugget, too. You can't know statically if you're inside a transacted block--unless you limit transactions to simple blocks w/out function calls--so when you JIT a memory access, it's pretty damn tough to figure out the right thing to do. Ultimately, you don't want to add a level of indirection for each memory read/write. But this is what I've done thus far, mainly because it's a step in the right direction. Further, you can't safely retry stuff that does I/O, so I'm building a constant list of “known“ I/O routines in the BCL. This is crap and doesn't scale. I wish we had monads in the runtime... or at least some form of annotation so that I knew what library call ended up doing I/O.
  • Did I mention I have a book to write?

I was thinking tonight: it'd be freaking awesome to have a pure functional language based on Haskell's laziness, pattern matching, and approach to I/O (monads). But with a paranthetic syntax, along with lists like Lisp/Scheme. Wow. Does it exist?

Perhaps I'll go and *do* something now. Instead of just talking about doing something.

4/16/2005 10:27:05 PM (Pacific Daylight Time, UTC-07:00)  #    Comments [5]

I mentioned it a while back, but I have some level of platform interop working with Sencha. By “platform interop,” I mean writing code that uses functions defined elsewhere (i.e. not Scheme built-ins and not custom written stuff). The gunk that enables this usually ends up making a best guess at binding, in some cases performing operations to bridge the type system gap that exists.

One of the interesting things I noticed along the way was how easy it is to work with XML inside Scheme. I do some marshalling back and forth using factories so that, when you're in Scheme, you see S-expressions which can be processed and transformed as ordinary lists of data. When you're using Framework APIs that expect XmlReaders, Documents, and the like, however, they see what they expect. It's admittedly dangerous to perform this style of conversion implicitly, but for the time being it's been the source of some fun experimentation.

For example, generating SOAP is quite simple, etc.:

'(Envelope (:ns s)
     (@xmlns:s "
http://www.w3.org/2003/05/soap-envelope")
     (@xmlns:wsa "
http://schemas.xmlsoap.org/ws/2004/08/addressing")
     (@xmlns:f123 "
http://www.fabrikam123.example/svc53")
   (Header (:ns s)
     (MessageID (:ns wsa) "uuid:aaaabbbb-cccc-dddd-eeee-ffffffffffff")
     (ReplyTo (:ns wsa)
       (Address (:ns wsa) "
http://business456.example/client1"))
     (To (@mustUnderstand "1") (:ns wsa) "
mailto:joe@fabrikam123.example")
     (Action (:ns wsa) (@mustUnderstand "1")
       "
http://fabrikam123.example/mail/Delete"))
   (Body (:ns s)
     (Delete (:ns f123)
       (maxCount 42))))

Similar to what's possible in C-omega, quasiquotations enable you to embed calculations in the message. For example, the body node could have been:

'(Body (:ns s)
  ,(generateSoapBody ,42))

Which has the nice effect of substituting the return value of the generateSoapBody function, passing 42 as its argument.

As I said, the greatest thing about this is that you can use all of the list processing techniques that Lisp langauges are good for, existing libraries, and so on, and then easily convert the result back into XML. Parsing, schema and namespace validation, resource resolution (e.g. DTDs) is all done for you by the existing System.Xml Framework libraries.

I know that the linkage between the two technologies has been observed in the past, but it seems like there's a lot of room for innovation in the future. Update: just noticed this page. Some interesting stuff.

3/30/2005 3:17:58 PM (Pacific Daylight Time, UTC-07:00)  #    Comments [2]

I’m in the process of writing up several articles/whitepapers that I intend to start publishing over the coming weeks.

  • Dispose, Finalization & Resource Management Design Guidelines: I’ve been iteratively working on these updates over the past couple months and just submitted the whole thing to our core review group to solicit feedback, comments, and the like. It covers the recent unification work we did around the Dispose pattern in the Framework, along with general guidance on writing correct resource management code. For a bit of history around this, check out here and here. The end result turned out to be a bit longer and larger than I had originally intended, but I think I was able to lay it out in an intuitive and easily consumable way.
  • Writing Atomic Abort-Safe Code: A lot of people writing code for the Framework lose a bunch of sleep over writing the most reliable code possible. However, our guidance on how to do this in the face of asynchronous thread aborts hasn’t been clear in the past, especially when writing paired operations (e.g. acquire/release semantics). So a bunch of us got together, discussed it, and decided we need to come up with some clear guidelines. I’m writing up a brief article and following it up with a DG update and some FxCop rule proposals. Priority #1: convince most people that they needn’t worry about these things. Only then will the painful details of abort semantics, AD unloads, CERs, critical finalizers, finallys, and the like be presented.
  • Threading Security Best Practices Design Guidelines: During the recent Whidbey security push, we produced a lot of great content around how to write secure multi-threaded code. Unfortunately, for obvious reasons much of this must remain inside MS. But some of the more general guidance is being incorporated into the DG. This includes, for example, avoiding publicly-accessible locks, never accepting ReaderWriterLock LockCookies from untrusted sources, and avoiding message pumping inside a synchronized region. Hopefully this will help users of the Framework to write more robust and secure code, too.
  • Concurrency & Parallelism: These topics are more personal research interests of mine, but nonetheless somewhat related to my job as the threading PM. I walk through a number of aspects about concurrency, parallelism (yes there’s a difference), and the nature of each. Specifically with regards to parallelism, I discuss the intrinsic algorithmic properties which are conducive to parallel execution, some theoretical math which demonstrates that, with the right multi-x (where x is core|proc) and task management architectures, the future looks promising. Check out this butterfly sorter. I recently had an interesting conversation with its author, Satnam Singh, and we seem to agree on many things. This particular example was designed and implemented in specialized hardware, but there’s no reason why we can’t write such things generically in software to take advantage of the underlying hardware support. I present some interesting evidence and conclusions to support this assertion.
  • Compiling Scheme to IL with Sencha: Again, personal research area. I need to write up some findings based on my compiler work, what I consider to be the unique features of Sencha, and mostly just capture knowledge so I have something to refer back to. I’m thinking more seriously about another compiler effort, and have chatted with the GHC and MSR folks a bit about it. Basically, I intend to create a STG textual format and a corresponding STG-to-IL compiler, based on Simon’s Spineless Tagless G-Machine paper. I believe this could then be used easily as a backend to the GHC compiler. Very ambitious project with a limited user audience, but it seems to have a lot of interesting facets to it. Such software could be used for parallel research on the CLR in the future.
3/12/2005 4:05:55 AM (Pacific Standard Time, UTC-08:00)  #    Comments [2]

Spent a little time hacking on forward interop between Sencha and the Framework, that is enabling Scheme programs to call into existing libraries. For now, I settled on a new callp function (stands for “call platform”) which does some heavyweight reflection-based lookup and invocation. It takes a varying number of arguments, and if the best match is non-static, the first argument is used as the target of the invocation. The assemblies to search are defined at the command line, similar to C#'s /reference:x switch.

As an example of a few simple calls:

(callp "Console.WriteLine" "Hello, World!") -> #t
(callp "Math.Max" 32.5 59.2) -> 59.2
(let ((stream
      (callp "System.IO.StreamReader.ctor" (callp "System.IO.File.OpenRead" "somefile.txt"))))
      (write (callp "System.IO.StreamReader.ReadToEnd" stream))
      (callp "System.IO.StreamReader.Close" stream)) -> #t

A bit verbose, yes, but I'm trying to avoid extending the language and instead extending the standard library. The “-> x” part is what the result of the expression evaluates to. Any methods with void returns translate into true, and a failure to locate a method translates into false. Admittedly, not optimal (for example with legitimate boolean return types where false is semantically meaningful), but it's at least got me cooking with gas for now.

1/22/2005 12:57:41 AM (Pacific Standard Time, UTC-08:00)  #    Comments [1]

Sweet. Tail calls were a bit easier to implement than I had expected. Still not 100%, but getting close.

At first, this was pretty damn difficult since I treat the result of evaluating a lambda as a delegate. E.g. a binding always ends up treating a variable bound to a lambda as a typed delegate (which points to the function that was generated). Unfortunately, delegates can't be tail called in the CLR.

So my first change was to start using the raw method handles instead of delegates where possible. This was an optimization I had to make anyhow, and it's had benefits elsewhere in the compiler that I just got for free. Then I changed my letrec implementation so that it directs the binding information down the lambda's AST before emitting its code. This way the lambda knows what it's being bound to (if anything), and adds it to a psuedo environment when generating its body.

I used to set the environment up for the lambda eagerly, but once I start referring recursively to the lambda being bound, it gets to be quite difficult! I had originally thought I can emit dummie calls to a static void NoOp() function and do some backpatching afterwards, but it turns out Reflection.Emit doesn't allow you to change the IL you've already emitted. So I wound up with the syntax-directed design.

Anyhow, I need to write up some good whitepapers on this stuff. Bottom line, this Scheme program:

(let ((fact2 (lambda (n v)
                  (if (> n 0)
                    (fact2 (- n 1) (* v n))
                    v)))
       (fact (lambda (n) (fact2 n 1))))
  (fact 1000000))

Used to bomb out with a StackOverflowException. Now it runs beautifully... well, except for the fact that I don't have bignum support and hence the result is “Infinity”. Nonetheless, it was a straightforward excercise.

1/20/2005 5:50:56 PM (Pacific Standard Time, UTC-08:00)  #    Comments [1]

I'm ashamed.

Well, the good news is, Sencha compiles simple programs like this:

(let ((sq (lambda (x) (* x x)))
      (dbl (lambda (x y) (x (x y)))))
  (dbl sq 10))

Which demonstrates nothing but good old environment modification and free variable binding.

But it also compiles this:

(letrec ((fib (lambda (x)
             (if (<= x 1)
                1
                (+ (fib (- x 1)) (fib (- x 2)))))))
     (fib 12))

Which demonstrates, of course, recurisve let bindings. This isn't what I'm ashamed of.

What I am ashamed of is the nasty IL that gets generated from the latter.

First, the top-level execution:

.method public hidebysig static void  Main(string[] A_0) cil managed
{
  .entrypoint
  // Code size       69 (0x45)
  .maxstack  3
  IL_0000:  newobj     instance void __lambda0::.ctor()
  IL_0005:  dup
  IL_0006:  ldvirtftn  instance object __lambda0::Apply1(object)
  IL_000c:  newobj     instance void class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>::.ctor(object,
                                                                                                                native int)
  IL_0011:  stsfld     class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object> Program::fib
  IL_0016:  ldsfld     class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object> Program::fib
  IL_001b:  dup
  IL_001c:  ldc.i4.1
  IL_001d:  call       void [SenchaRuntimeLibrary]Sencha.Runtime.RuntimeHelper::AssertCompatableFunctionType(object,
                                                                                                             int32)
  IL_0022:  castclass  class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>
  IL_0027:  ldc.r8     12.
  IL_0030:  box        [mscorlib]System.Double
  IL_0035:  call       instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>::Invoke(!1)
  IL_003a:  call       string [SenchaRuntimeLibrary]Sencha.Runtime.RuntimeHelper::ToString(object)
  IL_003f:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0044:  ret
} // end of method Program::Main

Notice the obvious areas for optimization... For example, if I construct a new Func1`2 delegate right before I call it... well... do I really need all that crap about reloading and type checking? Likely not.

But it gets worse. Check out the actual fib function IL:

.method public hidebysig virtual instance object
        Apply1([in] object x) cil managed
{
  .override  method instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Closure1`2'<object,object>::Apply1(!1)
  // Code size       189 (0xbd)
  .maxstack  10
  .locals init ([0] object[] V_0,
           [1] object[] V_1,
           [2] object[] V_2)
  IL_0000:  ldarg.1
  IL_0001:  ldc.r8     1.
  IL_000a:  box        [mscorlib]System.Double
  IL_000f:  call       bool [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::op_LessThanOrEqual(object,
                                                                                                             object)
  IL_0014:  box        [mscorlib]System.Boolean
  IL_0019:  call       bool [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::IsTrue(object)
  IL_001e:  brfalse.s  IL_0033
  IL_0020:  ldc.r8     1.
  IL_0029:  box        [mscorlib]System.Double
  IL_002e:  br         IL_00bc
  IL_0033:  ldc.i4.1
  IL_0034:  newarr     [mscorlib]System.Object
  IL_0039:  stloc      V_0
  IL_003d:  nop
  IL_003e:  nop
  IL_003f:  ldsfld     class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object> Program::fib
  IL_0044:  dup
  IL_0045:  ldc.i4.1
  IL_0046:  call       void [SenchaRuntimeLibrary]Sencha.Runtime.RuntimeHelper::AssertCompatableFunctionType(object,
                                                                                                             int32)
  IL_004b:  castclass  class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>
  IL_0050:  ldc.i4.1
  IL_0051:  newarr     [mscorlib]System.Object
  IL_0056:  stloc      V_1
  IL_005a:  nop
  IL_005b:  nop
  IL_005c:  ldarg.1
  IL_005d:  ldloc.1
  IL_005e:  ldc.i4.0
  IL_005f:  ldc.r8     1.
  IL_0068:  box        [mscorlib]System.Double
  IL_006d:  stelem.ref
  IL_006e:  ldloc.1
  IL_006f:  call       object [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::op_Sub(object,
                                                                                                   object[])
  IL_0074:  call       instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>::Invoke(!1)
  IL_0079:  ldloc.0
  IL_007a:  ldc.i4.0
  IL_007b:  ldsfld     class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object> Program::fib
  IL_0080:  dup
  IL_0081:  ldc.i4.1
  IL_0082:  call       void [SenchaRuntimeLibrary]Sencha.Runtime.RuntimeHelper::AssertCompatableFunctionType(object,
                                                                                                             int32)
  IL_0087:  castclass  class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>
  IL_008c:  ldc.i4.1
  IL_008d:  newarr     [mscorlib]System.Object
  IL_0092:  stloc      V_2
  IL_0096:  nop
  IL_0097:  nop
  IL_0098:  ldarg.1
  IL_0099:  ldloc.2
  IL_009a:  ldc.i4.0
  IL_009b:  ldc.r8     2.
  IL_00a4:  box        [mscorlib]System.Double
  IL_00a9:  stelem.ref
  IL_00aa:  ldloc.2
  IL_00ab:  call       object [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::op_Sub(object,
                                                                                                   object[])
  IL_00b0:  call       instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>::Invoke(!1)
  IL_00b5:  stelem.ref
  IL_00b6:  ldloc.0
  IL_00b7:  call       object [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::op_Add(object,
                                                                                                   object[])
  IL_00bc:  ret
} // end of method __lambda0::Apply1

Ouch! You mean I have to generate verifiable code and optimize it?!? As each day passes, I respect commercial compiler teams a little more.

In case you're wondering, the roughly equivalent C# code would be:

using System;

class Program
{

  delegate double fibfunc(double x);
 
static fibfunc fib;

  static void Main()
  {
    fib = delegate (double x) {
      if (x <= 1)
        return 1;
      else
        return fib(x - 1) + fib(x - 2);
    };
    Console.WriteLine(fib(12));
  }

}

Believe me -- the IL generated is a bit nicer. :)

1/20/2005 12:10:47 AM (Pacific Standard Time, UTC-08:00)  #    Comments [2]

Does anybody out there know of any Groovy-like languages currently being developed for the CLR?

As much as I love working on Sencha, I'm coming to the point where some of the more esoteric features of Scheme are beginning to surface. (Read: more time consuming and difficult to implement.) These include macros, continuations, and all of the proper edge cases of tail calls. I intend to invest further in the project, but have begun to think about other related efforts a bit more realistically. I'm at the point where I can almost compile all R5RS compliant syntax minus the aforementioned challenges, and the amount of midnight oil I have left to expend is pretty limited at this point. I need to invest it wisely.

In particular, I'm interesting in developing a language which is a mini-superset of C#. It's a bit odd to say "mini" and "superset" at the same time, but I'm thinking of starting out initially by paring away languages features (e.g. making all type annotations optional--relying on inferencing where possible and dynamic typing otherwise, relaxation of namespace declarations (i.e. "using" and auto-searching linked assemblies, reporting error upon resolution failure), runtime interpretation and execution) and then extending it with some interesting facets (e.g. non-delegate based closures, declarative concurrency, structural typing, more inferencing all over the place in addition to namespace declarations and typing (main method loops, and so on), plus more).

I've already found a name for it. C♭ (pronounced "see flat").

Any thoughts or input on such an effort would be apprciated. Ciao.

1/16/2005 10:55:45 PM (Pacific Standard Time, UTC-08:00)  #    Comments [8]

So I've decided to go back and try to do some static type checking for Sencha. This is for a few reasons:

  • It seems too easy not to do.
  • Many of the one-off optimizations I was contemplating would have all been solved by a more general typing strategy.
  • I'm interested in the area of type soundness in compiler implementations more so than many others, so it's a bit selfish, too.

I injected a new backend phase which traverses and "rewrites" the AST to contain typing information before doing code generation. That is, each node in the tree is given an evaluation type. The two base cases are: 1) function evaluation, where the evaluation type is the return type; and, 2) literals, where the evaluation type is the literal type. Everything else just borrows the type from one of its subordinates. There are lots of cases where I can still get stuck, however, and have to back out and resort to type erasure in such situations. This means using object everywhere, with lots of boxing and casting. This tends to have a viral nature up the AST, as once I start using erasure on a less elder node, everyone further up in the tree starts to see object as the inherited type. As an example, consider the Scheme if statement:

<if-stmt> ::= (if <test> <consequent> <alternate>)

This has the type:

<consequent> : T, <alternate> : T
-----------------------------------
<if-stmt> : T

<consequent> : T1, <alternate> : T2
-----------------------------------
<if-stmt> : typeof(object)

In other words, if <consequent> and <alternate> are both of the same type T, the type of <if-stmt> is T. Otherwise, we erase to object, and ensure to box up whatever is left on the stack; that is, if <test> evaluates to true and T1 is a value type, we must box; if <test> is false and T2 is a value type, we must box.

The chances to get stuck are numerous right now, and not just limited to funky type mismatches like that mentioned above. This is primarily my own fault, as any lambda gets generated as having an object return value. I theoretically can support arbitrary return types since each lambda is represented by a generic delegate (T1 Func1<T1,T2>(T2 a), T1 Func2<T1,T2,T3>(T2 a, T3 b), T1 Func3<T1,T2,T3,T4>(T2 a, T3 b, T4 c), and so on). But for now, I just make sure to box everything up before passing it to such a function as an argument, and ensure that from within the delegate I box any value types before returning. The bottom line here is that any time I encounter an application of a first class function, I have to resort to complete erasure of its arguments and return value which ripples up the AST. In Scheme, this is obvioulsy a pretty pervasive idiom, so I will certainly put the effort in to make this better.

The primary benefits right now are small optimizations, where I can now ask the simple question: "What type does this expression evaluate to?" and make optimizations (such as avoiding boxing) based on that. Moreover, I can bind to more appropriate overloads of primitive and Framework methods since I know the type in most cases. My example from yesterday's post with the (+ 1 2 3 4 5) Scheme expression looks a bit better now. Since I can now bind to the type-safe version, the IL looks like this:

.method public hidebysig static void  Main(string[] A_0) cil managed
{
  .entrypoint
  // Code size       91 (0x5b)
  .maxstack  4
  .locals init ([0] float64[] V_0)
  IL_0000:  ldc.i4.4
  IL_0001:  newarr     [mscorlib]System.Double
  IL_0006:  stloc      V_0
  IL_000a:  nop
  IL_000b:  nop
  IL_000c:  ldc.r8     1.
  IL_0015:  ldloc.0
  IL_0016:  ldc.i4.0
  IL_0017:  ldc.r8     2.
  IL_0020:  stelem.r8
  IL_0021:  ldloc.0
  IL_0022:  ldc.i4.1
  IL_0023:  ldc.r8     3.
  IL_002c:  stelem.r8
  IL_002d:  ldloc.0
  IL_002e:  ldc.i4.2
  IL_002f:  ldc.r8     4.
  IL_0038:  stelem.r8
  IL_0039:  ldloc.0
  IL_003a:  ldc.i4.3
  IL_003b:  ldc.r8     5.
  IL_0044:  stelem.r8
  IL_0045:  ldloc.0
  IL_0046:  call       float64 [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::op_Add(float64,
                                                                                                    float64[])
  IL_004b:  box        [mscorlib]System.Double
  IL_0050:  call       string [SenchaRuntimeLibrary]Sencha.Runtime.RuntimeHelper::ToString(object)
  IL_0055:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_005a:  ret
} // end of method Program::Main

Note that if I were to do something like (+ 1 2 ((lambda () 3)) 4 5), it breaks down immediately and erases everything. It should be obvious that this can bind to the op_Add(double, double[]) overload, but it actually can't. This is because, as stated above, all lambdas return object. Thus, when I evaluate ((lambda () 3)), the evaluation type of this expression turns out to be object. This will be a focus for optimizations in the future.

12/31/2004 1:14:31 AM (Pacific Standard Time, UTC-08:00)  #    Comments [0]

I have about 75% of the standard R5RS functions implemented now. I chose to write the runtime library in C#, primarily because I wanted to make heavy use of the existing Framework and haven't shored up my Scheme<-{OtherIL} interop just yet. It's amazing how simple the mapping was. Most Scheme functions ended up being one- or two-liners that just marshalled through to existing APIs. Man, I love the Framework.

When I say {OtherIL}, I mean simply accessing C#-compiled code from Scheme, for example. It's trickier than it sounds. First, I need to figure out how to do lookups and bind to exiting .NET functions. I.e. do you simply access by the class's FQN and then search all referenced assemblies at compile-time for a match? Require assembly annotations? Introduce non-Scheme-compliant syntax? And so on. For the Scheme library functions, I have a controlled manner of adding them to the environment and accessing them.

Moreover, I can (and do!) make a lot of assumptions about consuming code I've compiled since... well... I compiled it. For example, all of the standard Scheme functions leave something on the stack after executing. (I.e. non-void return types.) Scheme differentiates between commands and expressions, the latter of which always return a value. Once I introduce methods with void return types, a lot of things will stop working, including a small feature called verifiability.

Anyhow, I've chosen to use the new Whidbey LinkedList<T> class for lists. This means that when you do a (cons x y), you're really emitting a new LinkedList in the background. (car x) will simply return the head of the list, while (cdr x) will return the portion of the list following the head. It's unfortunate, but I had to write a "sublist" function myself since the Framework doesn't have one. I chatted with Krzysztof about this, but it seems this is just the way the cookie crumbles! It was relatively simple anyhow, although not entirely trivial since a node maintains an affinity to a particular list (so new LinkedList<T>(otherList.Head.Next) won't work).

private static LinkedList<T> CloneSubList<T>(LinkedListNode<T> node)
{
    LinkedList<T> newList = new LinkedList<T>();
    while (node != null)
    {
        newList.AddTail(node.Value);
        node = node.Next;
    }
    return newList;
}

So lists are surprisingly working almost perfectly with very little work! Even the 30-odd permutations of cadr, cddar, cdadadr, cdaaaddddaaddadddaaddr, and so on. :) I really wish there were better sublist support, though, as this is a horribly inefficient way to do business. Not to mention that I think the semantics will break down at some point, for example when using calls such as (set-cdr! (cdr x) y), as it mutates the copy of the list, not the original one. Still a little work to do here. Damn all of the x! methods in Scheme! They make things just a little bit trickier... should've stuck to a Haskell implementation.

Finally, after ratholing a bit on trying to static type check (solely through inferencing), I chatted with Jim a bit on Tuesday. Python is, along with Scheme, almost always purely dynamically typed, so I was curious what he did with IronPython. Seems like he does some interesting optimizations such as pooling boxed value types, but has a similar approach to mine. As I noted earlier, this means a hell of a lot of boxing and unboxing, along with casts galore to make the verifier happy. One snippet of ridiculous IL that gets created for an if statement is as follows.

This is the Scheme code:

(if #t
    'success
    'failure)

And this is the IL:

.method public hidebysig static void  Main(string[] A_0) cil managed
{
  .entrypoint
  // Code size       43 (0x2b)
  .maxstack  2
  IL_0000:  ldc.i4.1
  IL_0001:  box        [mscorlib]System.Boolean
  IL_0006:  unbox      [mscorlib]System.Boolean
  IL_000b:  ldind.i1
  IL_000c:  brfalse    IL_001b
  IL_0011:  ldstr      "success"
  IL_0016:  br         IL_0020
  IL_001b:  ldstr      "failure"
  IL_0020:  call       string [SenchaRuntimeLibrary]Sencha.Runtime.RuntimeHelper::ToString(object)
  IL_0025:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_002a:  ret
} // end of method Program::Main

See any opportunity for optimization? Anyhow, this is a bit tangential, and is just a simple optimization I need to work on. The more worrysome fact is that I've created strongly typed overloads for all of the Scheme library functions, but can't use them right now! For example, I have:

[SchemeFunction("+")]
public static object op_Add(object z1, object[] zn);
[SchemeFunction("+")]
public static double op_Add(double z1, double[] zn);

Currently, I only ever bind to the first one. Which means stupid looking calls such as the following compilation.

Scheme:

(+ 1 2 3 4 5)

IL:

.method public hidebysig static void  Main(string[] A_0) cil managed
{
  .entrypoint
  // Code size       111 (0x6f)
  .maxstack  4
  .locals init ([0] object[] V_0)
  IL_0000:  ldc.i4.4
  IL_0001:  newarr     [mscorlib]System.Object
  IL_0006:  stloc      V_0
  IL_000a:  nop
  IL_000b:  nop
  IL_000c:  ldc.r8     1.
  IL_0015:  box        [mscorlib]System.Double
  IL_001a:  ldloc.0
  IL_001b:  ldc.i4.0
  IL_001c:  ldc.r8     2.
  IL_0025:  box        [mscorlib]System.Double
  IL_002a:  stelem.ref
  IL_002b:  ldloc.0
  IL_002c:  ldc.i4.1
  IL_002d:  ldc.r8     3.
  IL_0036:  box        [mscorlib]System.Double
  IL_003b:  stelem.ref
  IL_003c:  ldloc.0
  IL_003d:  ldc.i4.2
  IL_003e:  ldc.r8     4.
  IL_0047:  box        [mscorlib]System.Double
  IL_004c:  stelem.ref
  IL_004d:  ldloc.0
  IL_004e:  ldc.i4.3
  IL_004f:  ldc.r8     5.
  IL_0058:  box        [mscorlib]System.Double
  IL_005d:  stelem.ref
  IL_005e:  ldloc.0
  IL_005f:  call       object [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::op_Add(object,
                                                                                                   object[])
  IL_0064:  call       string [SenchaRuntimeLibrary]Sencha.Runtime.RuntimeHelper::ToString(object)
  IL_0069:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_006e:  ret
} // end of method Program::Main

Gawd, that hurts the eyes! It's quite obvious I can inference in this case (as everything's a ldc.x), but it begins to break down quick enough.

His advice was to forego any attempts at type inferencing, or even the thought of adding static type annotations, until the purely dynamic version is working. I had started to come to the same conclusion after spending a lot of time on this. This is a tough problem to solve, even for languages that have static type annotations!

To finish the post off, here is a set of simple tests that now work correctly. I had on the order of 30 tests functioning now. Only about 150 to go. :)

(number? 35.05)
=> #t

((lambda (x y)
   (x y))
   (lambda (x)
     ((lambda (x) (* x y))
     x))
   3)
=> 9

(let ((gle (lambda (x y)
             (cond ((> x y) 'greater)
                   ((< x y) 'less)
                   (else 'equal)))))
     (gle 3 10))
=> less

(car (cons 3 5))
=> 3

(cadr (cons 3 5))
=> 5

12/30/2004 12:32:29 AM (Pacific Standard Time, UTC-08:00)  #    Comments [1]

I can now compile simple Scheme function calls into IL, such as

(+ 5 10)

As well as lambda applications

((lambda (x)
   (* x x))
 9)

((lambda (x y)
   (* x y))
 9 3)

And I was just able to compile (a workable copy!) of the following function call. It was a little tricky because the first lambda deals with higher-order functions... i.e. it takes a function as its first argument:

((lambda (x y)
   (* (x y) y))
 (lambda (x) (* x 2)) 3)

While it may not be immediately obvious, the correct output for this is 18. Indeed, that is what gets printed. :)

Below is the IL that gets generated by my compiler for this last program. Notice the hideous overuse of boxing. This will change. I already have a bunch of so-called “fast” entry points for all of the standard Scheme functions, and I intend to do some trickery with the lambda closure classes. Since Scheme doesn't have static typing, anything I introduce is just an optimization, and will have to rely solely on the use of literals to infer the type. Everything else has to be run-time type checked, which means objects and boxed value types everywhere. I might be able to get a little clever here, but truthfully haven't spent enough time thinking about it.

Lambdas are represented as Closure objects which have corresponding Function delegates. This is how procedure calls to lambdas occur. You might be able to discern from the IL that evaluation of a lambda generates the new class, instantiates it (passing any free variables to its constructor), and leaves a delegate on the stack. This will not be changing... (Unless I find that performance is poor, of course. From some benchmarking, however, it seems that our Whidbey delegate invocation improvements reduce the overhead versus a straight virtually dispatched call to just under 10%.) One optimization I will certainly be making however, is avoiding the creation of a Closure-derived class altogether for simple lambdas without free variables. In this case, it'll just end up as a static method.

Anyhow, here's the code. Sorry I don't have time to go into great detail right now.

.class public auto ansi Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main(string[] A_0) cil managed
  {
    .entrypoint
    // Code size       59 (0x3b)
    .maxstack  4
    IL_0000:  newobj     instance void __lambda0::.ctor()
    IL_0005:  dup
    IL_0006:  ldvirtftn  instance object __lambda0::Apply2(object,
                                                           object)
    IL_000c:  newobj     instance void class [SenchaRuntimeLibrary]Sencha.Runtime.'Func2`3'<object,object,object>::.ctor(object,
                                                                                                                         native int)
    IL_0011:  newobj     instance void __lambda1::.ctor()
    IL_0016:  dup
    IL_0017:  ldvirtftn  instance object __lambda1::Apply1(object)
    IL_001d:  newobj     instance void class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>::.ctor(object,
                                                                                                                  native int)
    IL_0022:  ldc.r8     3.
    IL_002b:  box        [mscorlib]System.Double
    IL_0030:  call       instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Func2`3'<object,object,object>::Invoke(!1,
                                                                                                                        !2)
    IL_0035:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_003a:  ret
  } // end of method Program::Main

  .method public specialname rtspecialname
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  2
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method Program::.ctor

} // end of class Program

.class public auto ansi sealed __lambda0
       extends class [SenchaRuntimeLibrary]Sencha.Runtime.'Closure2`3'<object,object,object>
{
  .method public hidebysig virtual instance object
          Apply2([in] object x,
                 [in] object y) cil managed
  {
    .override  method instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Closure2`3'<object,object,object>::Apply2(!1,
                                                                                                                        !2)
    // Code size       14 (0xe)
    .maxstack  2
    IL_0000:  ldarg.1
    IL_0001:  ldarg.2
    IL_0002:  call       instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Func1`2'<object,object>::Invoke(!1)
    IL_0007:  ldarg.2
    IL_0008:  call       object [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::Multiply(object,
                                                                                                       object)
    IL_000d:  ret
  } // end of method __lambda0::Apply2

  .method public specialname rtspecialname
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  2
    IL_0000:  ldarg.0
    IL_0001:  call       instance void class [SenchaRuntimeLibrary]Sencha.Runtime.'Closure2`3'<object,object,object>::.ctor()
    IL_0006:  ret
  } // end of method __lambda0::.ctor

} // end of class __lambda0

.class public auto ansi sealed __lambda1
       extends class [SenchaRuntimeLibrary]Sencha.Runtime.'Closure1`2'<object,object>
{
  .method public hidebysig virtual instance object
          Apply1([in] object x) cil managed
  {
    .override  method instance !0 class [SenchaRuntimeLibrary]Sencha.Runtime.'Closure1`2'<object,object>::Apply1(!1)
    // Code size       21 (0x15)
    .maxstack  2
    IL_0000:  ldarg.1
    IL_0001:  ldc.r8     2.
    IL_000a:  box        [mscorlib]System.Double
    IL_000f:  call       object [SenchaRuntimeLibrary]Sencha.Runtime.StandardSchemeFunctions::Multiply(object,
                                                                                                       object)
    IL_0014:  ret
  } // end of method __lambda1::Apply1

  .method public specialname rtspecialname
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  2
    IL_0000:  ldarg.0
    IL_0001:  call       instance void class [SenchaRuntimeLibrary]Sencha.Runtime.'Closure1`2'<object,object>::.ctor()
    IL_0006:  ret
  } // end of method __lambda1::.ctor

} // end of class __lambda1

12/24/2004 3:17:01 AM (Pacific Standard Time, UTC-08:00)  #    Comments [1]

 

Recent Entries:

Search:

Browse by Date:
<September 2010>
SunMonTueWedThuFriSat
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

Browse by Category:

Notables: