Login [Register]
Don't have an account? Register now to chat, post, use our tools, and much more.
MateoConLechuga wrote:
It's been a while; any progress on the implementation side or is it still highly theoretical at the moment? Smile

Not much. Life is busy, and I work in small infrequent chunks. I've taken a long detour to get the "readme" to my liking, but I concede that a working demo will do that best.

MateoConLechuga wrote:
I was looking through some of the recent changes to Objects.js, and I must say I am rather impressed Smile

Thanks! Smile What about the recent changes are impressive?

MateoConLechuga wrote:
I would also just like to note that the first stage 'Make an interpreter', format seems exactly like what the current system is for most programming languages.

Well sure; but do my next 5 steps not paint a different picture?

MateoConLechuga wrote:
What really separates this from other languages?

It's honestly not substantially different from Scheme or JavaScript, though the ability to extend/modify the grammar at runtime is more like Forth.

But what I can't emphasize enough is that it's not about the language, it's about the system I will create with it.

Really, any language would do, if it could modify its own guts at runtime. But since none do that (not even in the LISP family! -- I'll post another reply to elaborate), I must entirely encode a new one into my system.

MateoConLechuga wrote:
I feel that languages are very limited by the hardware, not by the software implementation.

I disagree. My view is that language & expressiveness are separate issues from efficiency, especially considering how powerful computers have become over the years. Just think of how much is possible in JavaScript nowadays -- just look a "Lively Kernel" or "JS.JS", or the 3D game engines that people are making in JavaScript to run in the browser!

However, projects like mine and those would CERTAINLY be more practical if we weren't STILL stuck with microprocessor architecture from the 60's. Alan Kay talks about this, but also see Brett Victor's "The Future of Programming" or (more relevant to this project) "The Early History of SmallTalk" (where it says "the art of the wrap is the art of the trap"), both of which can be found on worrydream.com

On that note though, did you know that there *was* going to be an OOP based processor? Imagine if creating an "object" had hardware support? Imagine how much faster and more efficient things would be today (versus whatever stack sits between JavaScript on a web page, and the machine?), had marketing not made the designs of decades past look more lucrative at the time? Imagine if instead of a "hard" CPU with a set list of instructions, the hardware could allocate instances of Agents (Actors) with their own set of operations? ... Maybe someday. Though I do have high hopes for Web Assembly Very Happy
On the fact that LISP (and family) are not self-modifying: there is no way to inspect the structure of any existing functions! Sure, the code might START as data, but once it's passed into "defunc" or "define" or whatever, what you get out is something that is executable, but un-inspectable.

I'm interested in more than just a dynamic way of GENERATING software (macros do nicely for that, but then they disappear), but a structural representation of everything, so that EVERYTHING can be inspected and modified at runtime.

That is why my eval will walk through code without compiling it. It's also why I needed to wrap functions in an object that states what the argument names are and whether it's a "syntax" function (which is like a LISP macro, but that can be called any time, rather than just being stripped out at "startup"). My system will have no concept of "compile time", but that does not inhibit the ability to make code that generates other code.
So I've found one major mistake with my implementation, and that is that the Eval tries to chain "parent" and "child" contexts (the "env" parameter) across function calls, or as it climbs into "inner" expressions. The effect is a partial conflating of dynamic scope (bad) with lexical scope (good).

The only place out really needs to do this is within lambdas / function definitions, which create the parent scope set to the lexical scope that it is declared within. I'll need to update call-evaluation to retrieve that as the new "env", and then nested expression scope becomes a special case of this.
I've updated my "Eval Grammar" with the aforementioned correction. Essentially, when a function is called, a new scope object ("env") is created for that invocation, and its parent-scope is the lexical scope in which that function was created. The mistake before was that the scope of the calling function became the parent.

One concern this has raised is that this requires each function to store a reference to its scope (of which they are a part), thus resulting in circular references. That makes serialization difficult. However, this is in-escapable if I want to allow context-dependent definitions; and honestly, this is what is going on in most languages (how can one function call another if it does not contain a reference to it?). Also, I can get around serializing circular references by instead serializing code to create the functions into their contexts.

I thought maybe I could get around this by requiring context to be passed into a function, but that assumes that the caller knows it or has it ... unless ... I suppose closures could be created by generating code that hard-codes the right context into a function call. ... I'll think about that.
I figured my Eval Grammar was interesting enough to post here (with the aforementioned fix):

Code:
eval(expr, env)
:: ([x, ..], _)                --> call(lookup(eval(x, env)), [..], env)
:: (_      , _)                --> expr

lookup(key, env)
:: ("x", {x}     )             --> x
:: ("x", {parent})             --> lookup("x", parent)
:: (_  , _       )             --> null

call(func, args, env)
:: (native       , _, _)       --> native(getArgs(func, args, env), env)
:: ({body:native}, _, _)       --> native(getArgs(func, args, env), env)
:: ({body}       , _, _)       --> eval(body, newEnv(func, getArgs(func, args, env), env))
:: (_            , _, _)       --> null

getArgs(func, args, env)
:: ({syntax}, [x..], _)        --> [x..]
:: (_       , [x..], _)        --> [eval(x, env)..]

newEnv(func, params, env)
:: ({args:["a"..]}, [x..], _)  --> {parent:getScope(func), caller:env, args:params, a:x..}
:: (_             , _    , _)  --> {parent:getScope(func), caller:env, args:params}

getScope(func)
:: ({scope})                   --> scope
:: (_      )                   --> {}

(I have yet to make the corresponding code-changes for the fix)
Now that I've made those changes, I am closer to being able to bootstrap it (i.e. have it be written in itself and this be self-defined and self-modifying) than I thought would be the case at this point. Here's how:

Most of the code is now most just nested calls to the (so far) defined functions, and with a little more work (e.g. capture ifs and operators as functions), it can be 100% (excluding a few native functions, but those will always be needed). This could very easily be replaced with the same code objects that it is made to interpret (essentially nested lists, or S-Expressions). The "bootstrapper" will be a bit of JavaScript that compiles these back into JavaScript, which is simply a matter of converting it back into nested function-calls. The functions to call will be looked up dynamically, so the "compiler" does not need to know anything about how the eval (interpreter) actually works.

After that, I'll be able to do all future work (minus adding new native features) in terms of the language of the system. I can even write the compiler this way, and feed new versions to the compiled older versions of itself to get the new compiled version (which I then embed in the updated JavaScript to update the bootstrapper).

This is a bit different from my original plan to leave the INTERPRETER as the only compiled part, but I think this way will be more fluid, and easier to implement.
I realized a problem with the approach in my previous comment:

I cannot make a compiler that simple, unless it is restricted to a small subset of the language (which CAN be done without it "knowing" about any specific operations supported by the evaler). Otherwise it must either know about specific operations, which is bad because the evaler (interpreter) is intentionally decoupled from them as much as possible; or it must support all the same operations (e.g. "syntax" ops, a.k.a. "macros", etc.), which is essentially just reimplementing the evaler as a compiler.

It is important to allow the compiler (like everything else) to use the WHOLE range of operations supported for everything else, so that the system and the language of the system can freely be defined (and modified) in terms of each other.

I will have to come back to this later.
shkaboinka wrote:

On that note though, did you know that there *was* going to be an OOP based processor? Imagine if creating an "object" had hardware support? Imagine how much faster and more efficient things would be today (versus whatever stack sits between JavaScript on a web page, and the machine?), had marketing not made the designs of decades past look more lucrative at the time? Imagine if instead of a "hard" CPU with a set list of instructions, the hardware could allocate instances of Agents (Actors) with their own set of operations? ... Maybe someday. Though I do have high hopes for Web Assembly Very Happy

Don't see the point of an OOP CPU; an object is just a structure of data?..
c4ooo wrote:
Don't see the point of an OOP CPU; an object is just a structure of data?..


That is only true in statically-typed languages. In a dynamically-typed language, there are no "classes", and instead any object can have any property added to it at runtime.

This kind of flexibility allows for better (more direct) representations of ad-hoc human mental models, and more kinds of operations are possible with it.

A lot of people don't understand that value, and think that static typing is "better" because of the efficiency and compile-time checking. An OO CPU would make the first point untrue, and the second point is where we stop thinking about powerful new ways of thinking and doing because (as Gary Sussman puts it) "we are too busy diddling with our type systems".

Furthermore, low-level efficiency means nothing of the high level (end user) experience is slow and bad.

To state this whole problem on other words: Arches make terrible bricks
Thinking about how to compile from the language of the interpreter (eval) to native JavaScript has helped me realize that there are other things I must do (or decide) first.

Here is my TODO list:

- All native JS functions in the system need to be modified to comply with the uniform calling-convention. In other words, they must take arguments (cb, env), their current arguments need to be retrieved as properties of env, they must be written in CPS (done), and they must capture their lexical scope in a "scope" property (rather than as a JS closure).

- Add support for calling native JS that is NOT part of the system. Specifically, functions not supporting the aforementioned calling-convention will have their arguments passed DIRECTLY, and will be executed in direct style (rather than CPS).

- Just an idea: perhaps functions can contain an "eval" property which points to an alternate evaler implementation. This can make interop between DSLs work without each language having to know about each other (for example, a function in one language can be passed to a function in another language, and the one could call the other just by applying the referenced evaler to it). On the other hand, since this might require foreign functions to be "wrapped", the wrapper may as well consist of a direct call that passes the proper "function" to the correct evaler; which can already be done without having to add special "support" for it to the system.

- Just an idea: I might want to support "operator overloading" in objects. For example, an object could contain its own "lookup" (or "get") function, which the evaler would invoke instead of the normal one. This could also be a BAD idea, similar to how Jim Coplien calls inheritance polymorphism an "intergalactic goto". Also, if I add support for DSLs in the manner described in the previous item, then I can experiment with such language features / variations in an isolated manner, without committing the whole system to such choices.
shkaboinka wrote:

A lot of people don't understand that value, and think that static typing is "better" because of the efficiency and compile-time checking. An OO CPU would make the first point untrue, and the second point is where we stop thinking about powerful new ways of thinking and doing.

How can an OOP CPU be more efficient, and how would you even build an OOP CPU? In a CPU you have circuits that complete specific tasks (addition subtraction, bitwise math, FPU math, etc), and other circuits that parse commands and control the actions of reading/writing memory, and doing said mathematical operations. How can "oop" make mathematically intensive tasks such as physics, or tasks like passing data to a GPU or storage device faster?
c4ooo wrote:
How can an OOP CPU be more efficient, and how would you even build an OOP CPU?... addition subtraction, bitwise math, FPU math, ... parse commands ... reading/writing memory, ...


It's about direct hardware support for creating and manipulating OO (and other high level) structures (objects, lists, etc.). Create, lookup, get named property, remove property, send message (like calling a method, but more flexible and dynamic).

The operations you listed are low level, and also no different than what CPU already do. An OO CPU is not a new way of doing an old thing, but a new thing ... or maybe "almost" a new thing, as Alan Kay would put it.

Note that OO does NOT mean classes and polymorphism; that's an implementation detail. In this, object members would be looked up at runtime, rather than being "compiled away" into static offset indexing. If you think that's less efficient, it's only because one is supported by hardware, and the other is not. An OO CPU is about changing that.

This idea is also not that different from that of a LISP Machine. History dismissed them for being "pigs" (i.e. very resource intensive as compared to an equivalent assembly program). But again, that's comparing things in terms of low level operations like those you listed; and that was back in the 70's. If Moore's law holds up, today's computers are more than a million times more capable since then, and the typical layers (and frameworks) in most modern software is WAY more piggish by the same standards. An OO CPU would actually cut out a ton of those layers. Imagine something like JavaScript running DIRECTLY on the CPU.
A brief history of recent decisions, and their consequences:

But first for added context, here is a recap of some of what I'm after:

I've tried to blur the line between "the code of the system" (interpreter), and code running within the system (interpreted code). Both are made up of functions embedded within the same object-model, and everything operates by inspecting & modifying objects within that same model. Functions are either "native" (pure JavaScript), or are composed of (AST) objects that are interpreted by the system, but either kind should be callable from anywhere. Only the interpreter NEED to be "native" (otherwise the system cannot run). However, I will make an objects-to-native compiler so that EVERYTHING (except a few small fundamental operations) can be coded in the language of the system, thus making it self-(re)defining.

The system is implemented in Continuation Passing Style (CPS). Two main reasons is that this makes it much more possible to serialize (and save/restore) the state of a running system at any point, and so that operations that affect flow (e.g. "return") can be implemented as regular functions without any special support needed by the system.


1. There came a mismatch between how functions pass & return values. The "eval" function takes the code to be evaluated, and the context (lexical scope) in which to evaluate it (and similarly for helper functions, e.g. "lookup" which fetches a variable by name from the context). But since non-native code does not have direct access to the code-being-eval'ed and the context-object, I've changed things so that native (JavaScript) functions must always take the code-to-eval and the context-object as arguments. The "arguments" that are "passed" to the function (from the perspective of interpreted code) are actually added as properties of the context-object (nothing new here), so native functions access these directly as properties of the context-object, while still having direct access to what is being eval'ed and the context in which it is happening (e.g. so that new functionality can be added or modified).

2. The change from (1) would make it impossible to call arbitrary native-functions outside of the system. The change is to qualify compatible functions by wrapping them like object-functions. Object-functions are objects with properties specifying the arguments, whether it's a "syntax" (macro) function, the lexical scope ("scope"), and the "body" of the function. If the body is a native function, then it is invoked as stated above (1). If a native function is called directly, then the arguments are passed directly (rather than being nested inside a context-object), and are invoked in direct-style (rather than CPS -- see (6)).

3. Object-functions contain their own lexical "scope" object, effectively acting as a closure with the enclosed-scope being directly accessible as the "scope" property on the function-object. Since the scope of native javascript functions is inaccessible from within JavaScript (and because it "breaks" if you try to modify a function), I'm implementing the "native" operations as wrapped objects (see (2) above) and referencing neighboring functions via this "scope" property (which requires the setup of the system to carefully set that property to the object that contains everything). This allows even the native functionality to be modified directly without breaking contexts. (pure native functions are implemented in a way that they don't rely on any enclosing scope).

4. I chose not to have a "symbol" type, and instead use strings to look up variables. I did this because I did NOT want to wrap each value in an object with separate semantics for marking the type, and instead rely on the underlying JS type-system; and there is no matching underlying "symbol" type in JavaScript. The "uh-oh!" of this is that there is now no way to distinguish between looking up a variable and using a string value. For example, ("foo", "x") might mean to pass the value of variable x into function foo, or it might mean to pass the STRING "x" into foo. My plan for now is to use an object-wrapper to distinguish the two. I'm leaning toward wrapping symbols, so that strings can keep their simple implementation. Thus, ("foo", ("symbol", "x")). There is more to explore. Also, forget how "ugly" it might look, because remember that the goal is to map everything through a UI with the most ideal visual representation, which might not always be a 1:1 rendering of every property as it is literally laid out.

5. The presence of a "syntax" property on a (non pure-native) function tells the eval'er to pass the arguments as-is instead of evaluating them. Thus (foo (+ 1 2) (* 3 4)) will pass the actual (+) and (*) expressions into foo if foo is a "syntax" function; otherwise it will pass 3 and 12. This applies the same to object-wrapped native functions (maybe I need to name these better). These are LIKE "macro" functions in LISP languages, except that they can be called at any time rather than just being evaluated first and then removed. My reason for doing this is because the "macro" idea assumes a compilation-phase, whereas my system does NOT -- there is no "source" code (outside of bootstrapping the system); everything just IS. Perhaps later I will find something useful to do with the VALUE (rather than just the presence) of the "syntax" property.

6. The choice to implement the system in CPS (see top) means that native functions must also be (manually) implemented in CPS, and thus take a "callback" function as an argument. (non-wrapped native functions are still called in direct-style -- see (2)). I have yet to decide how to expose this callback to native functions, or whether I should implement call-cc. I might also consider augmenting the callback argument to include separate "success" and "error" callbacks, or perhaps even arbitrarily-labeled callbacks (e.g. for exception-handling).
(continuing from previous post)

7. I've provided a function for every "native" operation so that non-native code can use it, and as much as possible, I have native (JavaScript) code call these functions instead of just using native code directly. The idea was that even a fundamental operation (e.g. get a property from an object) could be modified, and doing so would affect all code that uses it. The problem with that though, is that the code I have so far is getting bloated and will also run slower because of all the indirection. I figured that this wouldn't matter since most of this would be (ultimately) generated from object-code to native code via the compiler (that is, after native-coding the whole thing into existence first). However, there are places where this would create infinite loops, and I cannot (for example) "look up" the look-up function. (I already break that rule by having the base functions refer to each-other directly rather than looking them up, for the same reason). SO, what I think I need to do is go ahead and apply fundamental operations directly (get/set/lookup, etc.) and code the objects-to-native compiler to follow the same rules.

What this means for the code right now, is that I can go through and make it much smaller and direct, which is nice for everyone looking at (or developing) it. Here's an example of what this change looks like (with some simplifications):


Code:
//BEFORE:
return tailcall(get, ["y", x], function (xy) {
  return tailcall(set, ["bar", foo, xy], function() {
    return tailcall(cb, [5]);
  });
});

//AFTER:
foo.bar = x.y;
return tailcall(cb, [5]);
I was one of a handful of people on the "FONC" mailing list for the Ian Piumarta's COLA Project (which originated from and fits well in line with the aspirations of VPRI), until an announcement was made that VPRI (being acquired by another organization) is no longer supporting that project, and the mailing list has been disbanded.

In response, several subscribers not only expressed their disappointment, but also shared their own personal projects and research (or at least, mentioned it), and as a result we decided that it might be beneficial to form a group or forum or whatever to share work & ideas on ... the kind of thing I am trying to achieve here.

As a result, I might add some new material to this project on github as a way to document & share some of the ideas, concepts, goals, and research related to "this kind" of stuff.

We'll see how that goes.
shkaboinka wrote:
I was one of a handful of people on the "FONC" mailing list for the Ian Piumarta's COLA Project ... the mailing list has been disbanded. ... In response, several subscribers ... shared their own personal projects and research ... it might be beneficial to (collaborate) ... I might add some new material to this project on github as a way to document & share.


I've actually created SomethingNew for this Smile
(I did it this way to separate it from MY project, since many projects may be involved)
shkaboinka wrote:

I've actually created SomethingNew for this Smile
(I did it this way to separate it from MY project, since many projects may be involved)


We've added a bunch of ideas, goals, and resources to this. I've also uploaded some great discussion on this that you might enjoy (since you're reading THIS to begin with). Go check it out!

One of the contributors (Joel) has A VERY similar journey to mine here. If you're at all interested in my project/discussion here (and perhaps even if not), I HIGHLY recommend you go read this post on his blog:

https://programmingmadecomplicated.wordpress.com/2017/08/13/language-isnt-everything/
I've uploaded my OLD implementation of "message passing" from the start of this thread, to my "Objects" repository: https://github.com/d-cook/Objects/blob/master/OO.js
shkaboinka wrote:
4. I chose not to have a "symbol" type ... I'm leaning toward wrapping symbols, so that strings can keep their simple implementation. Thus, ("foo", ("symbol", "x"))


For the time being, ("foo", ("lookup", "x")) already has meaning, since "lookup" is the function that looks up things (in current context, and then in parent context, etc.) by name.
shkaboinka wrote:
I am closer to being able to bootstrap it ... Most of the code is now most just nested calls to defined functions ... This could very easily be replaced with the same code objects that it is made to interpret ... the bootstrapper can compile these back into JavaScript [by] converting it back into nested function-calls. The functions to call will be looked up dynamically, so the "compiler" does not need to know anything about how the eval (interpreter) actually works ... After that, I'll be able to do all future work in terms of the language of the system
shkaboinka wrote:
I cannot make a compiler that simple, unless it is restricted to a small subset of the language ... It is important to allow the compiler (like everything else) to use the WHOLE range of operations supported for everything else, so that the system and the language of the system can freely be defined (and modified) in terms of each other
shkaboinka wrote:
I've provided a function for every "native" operation ... The idea was that even a fundamental operation could be modified, and doing so would affect all code that uses it ... However, there are places where this would create infinite loops, and I cannot (for example) "look up" the look-up function. (I already break that rule by having the base functions refer to each-other directly rather than looking them up, for the same reason). SO, what I think I need to do is go ahead and apply fundamental operations directly (get/set/lookup, etc.) and code the objects-to-native compiler to follow the same rules


Actually, I think I can (and will) be able to bootstrap the interpreter at this point, as I previously described. The only real "infinite loop" problem is looking up the lookup function. But since that is inescapable (I think), I will make that the one exception that is called directly, and all other functions can be looked up. Also, I think this only matters within the interpreter, since it's the thing that makes all the code work. Anything else can look it up indirectly because the "infinite lookup loop" will be broken in the interpreter.

So here is my strategy:

Code can be compiled from objects to JavaScript by replacing all the operations (functions) with calls to the "lookup" function (which will be referenced directly-ish), and based on the "scope" (closure) of the function (which is always passed-in, like how "this" is passed as an argument to JavaScript's "function.apply"). This approach will work without the compiler having to know about specific functions (other than "lookup"), because the "lookup" calls will be part of the compiled code (i.e. the compiler itself will not do any looking-up).

There are some "base functions" which must be defined in pure javascript, but everything else can be coded-up as objects (and only the interpreter needs to be compiled as part of the start-up bootstrapping process). The base functions include: Get, Set, Delete, Has, Type, Keys, If, Length, Call-CC, and operators like And, Or, Not, + - * / % = < >, etc. Those can just "sit" as is in the object-tree, of which everything is a part, to be looked-up like anything else (the point being that the compiler or interpreter does not need to know about them).
  
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 4 of 5
» All times are GMT - 5 Hours
 
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum

 

Advertisement