> Juice's distribution format ... is based on a tree-shaped program representation as is typically used transiently within optimizing compilers. Rather than containing a linear code-sequence that can be interpreted byte-by-byte, a Juice-encoded applet contains a compressed tree that describes the actions of the original program. The tree preserves the control-flow structure of the original program, which makes it much easier to perform code optimization while the tree is translated into the native instruction set
Wow, that came out of the left field. Shipping a lexical compiler IR and letting the target do the code generation sound like a very powerful technique compared to compiling for virtual targets like WASM, JVM etc. Wonder why it didn't caught on.
Another intriguing tidbit:
> By the very definition of our tree-encoding scheme, it is impossible to encode a tree that violates scoping rules of our source language. Such an "illegal" program cannot even be constructed by hand.
> Shipping a lexical compiler IR and letting the target do the code generation sound like a very powerful technique compared to compiling for virtual targets like WASM, JVM. Wonder why it didn't catch on.
This is somewhat similar to what V8 does with JIT compilation of javascript by now. I like the idea of doing it on AST rather than direct (heavier) source code. But maybe in the long run, gzipping the JS stream turned out to be just enough?
Gzipping the JS stream is conceptually very different, though. With Juice's approach, the intent is in part that the compiler gets to do any amount of additional work before serializing. What is shipped is already "half compiled", not just serialized as a tree. It's closer to an intermediate representation in tree form than just a representation of the source syntax.
Today, with a lot of JS toolchains involving source-to-source transformation first, that could in theory achieve much of the same potential of modifying the original input, and lowering some things to a simpler subset etc., but just compression isn't the same.
The size reduction is pretty much a nice bonus with Juice rather than the point - the dictionary encoding matters because it means the decode gets similarity measures "for free" and can generated patchable code segments for partial subtrees that will be subsequently referenced.
It's a direct continuation of Franz PhD thesis in '94 [1]. (I note I know need to keep my URLs stable, as it turns out one of the references to SDE in that Wikipedia article is my own summary...).
I think there are two reasons it didn't catch on: The first obviously being Java, and the sheer amount of mindshare Sun was able to get for Java very quickly.
The other is that Oberon had gaps. It's a very austere language. And while all of those gaps can be plugged - Franz himself proposed extensions, as did other students of Wirth - without someone putting in the effort to create an Oberon version with sufficient expansions to appeal to business users, the way Sun did with Java, that the underlying technology was interesting didn't really matter.
I'm a big fan of the concept, but it's also more complex in some ways, even though a crude implementation is also potentially very simple. E.g. you "chop the compiler in two", and unlike a bytecode compiler you get a tree to work on. At the same time you can apply other JIT techniques - part of the work on SDE meant reconstituting the dictionary used to compress the tree on loading, and while doing so, if you generate code during the process, you can also directly reference the generated code to use as templates for a patching JIT and insert those templates directly into the dictionary.
"One day" I hope to get time to explore SDE more, because it seems JIT's have gotten so far down the bytecode path that we're missing out on seeing what else might be viable.
Franz own later research shifted a lot towards Java, but his students have interesting work in the JIT space too. Especially Andreas Gal, and his work on trace trees.
Isn't it limiting? Say, the language evolves and its scoping rules change, new control flow features are added etc. Now you can't ship anything in the new version because of the old clients. With bytecode, language implementers have more freedom to change the language without having to force everyone to use a new runtime version. I guess it's not a problem if the language implementers also control the client (say, V8 in Chrome) but in case of Juice, they were just another third-party extension to Netscape, so overall it sounds like a bad idea to me (if I understood it correctly that they serialized and shipped AST to end users).
You can read Franz' PhD dissertation on it here[1]. While it's reasonable to describe it as serialising an AST, most of the Oberon compilers of the time did not have an AST per se, and so what you serialize is entirely arbitrary to your choice of node types, and the operations used by the Oberon version are low-level enough that while some work would have needed to be done to be generic enough if one wanted to more easily support other languages, it's largely moot. There's no large semantic gap between what is serialized and the generated code.
At that point it's no different from with Java - if you add new bytecodes, the JVM needs to support it. Same here - if you add new node types, the code generator will need to support it.
Just as you wouldn't change the JVM bytecodes for every language change, you likewise wouldn't need to add new node types to support most language changes for Juice either. You could just as well choose to generate what in effect would be an intermediate representation instead. You'd only add new node types if they provide a sufficiently big win over generating more complex output, just as with the JVM
That said, nothing would've prevented dynamically updating the code generator.
It would also make client-side verification easier. You could statically analyze code for safety before running it. You could optimize it much further for your platform. Safe mashups might be easier. Quite a few benefits to this.
One more is that, if you used an Oberon-based OS (eg A2 Bluebottle), the system and browser languages were the same. No need to learn separate languages. Integration will be smoother, too.
Interesting to see that Oberon-2 tried to go where Java succeeded. I guess java worked better because of language limitation. From my experiences in Oberon-2 (I work with it), I find the following limitations enough to explain it.
- No generic types. That's really limiting. You want to implement Linear Algebra for vectors ? You need a class for each possible types.
- No proper error handling : no such thing as `TRY` or `CATCH`. Instead, you need to pass a `BOOLEAN` mutable flag in functions that may crash. That's just sad.
- No distinction between a class, an abstract class and interfaces / pure abstract classes. If you want to write an abstract class, you just `HALT` in the base methods (like you would do in Python I guess). But there is no language semantic to define that this class is abstract. So the compiler can't help you out much with them.
- No enumerations. They are just very powerful.
(- Faulty garbage collector, even though that is not a a feature of the language but rather of the implementation that I am using.)
I am quite sure that Oberon was written with a real focus on designing a language for which it is easy to write a compiler / garbage collector. It really made sense back then. And they succeeded in doing so. However, for broad distribution of executable, it probably wasn't the best guess. These glimpses of the past are so interesting though, discovery of the most optimal tech stack was a long and dense path of exploration !
There were proposals for mechanism for extending types from several of Wirth's students, and Franz - one of the co-authors of Juice - was one of the people who around that time wrote a report on a proposal that'd enable runtime-loadable interface-based extension, so all of this would certainly be fixable.
Juice also flows naturally from Franz PhD thesis, which was on JIT compilation of Oberon from what in some sense was a compressed serialized reduced form of the AST instead of bytecode [I'll stress this is a gross oversimplification - his compiler does do more processing, so I guess what is serialized is closer to an IR in tree form than the AST]. I don't know how much that changed in Juice, as I've never looked at it much but from the description it remains at least similar in concept. I've wished more work had gone into that ever since I read his PhD thesis in '94.
On a phone and without a reference handy but search for Franz and "Protocol Extension". It's a very short paper, and basically proposed a mechanism similar to vtables, but w/an extra level of indirection to allow whole interfaces to be slotted in, and to have subsequent loaded implementations addressed propagated down the class hierarchy on load.
(I use a similar mechanism of propagating vtables updates down the class hierarchy, but without the extra indirection - at cost of more memory - in my prototype Ruby compiler)
Thank you, I got it: "Protocol extension, a technique for structuring large extensible software systems" [0] ! Hoping to find time to read it soon, and also to look at your ruby compiler !
My Ruby compiler is, I'm sad to say, languishing. I got it to a stage where it does compile itself successfully (to 32 bit x86 assembly - 32 bit was still a reasonable choice when I started...), but it has plenty of bugs (and only compiles itself due to workarounds for those in the compiler source to avoid triggering them) and doesn't support most newer Ruby syntax, nor Regexp's or Float's (other massively limitations too). I keep telling myself I should at least fix the bugs at some point and strip out the workarounds, but so many other projects at the moment.
About the what, I can't tell you too much unfortunately. We do realtime OS for autonomous robots in Oberon. About the why, it's a question of heritage from the past. The code works, so why taking the risk to migrate it. It's really a big code base, so migrating it would be (will be...) very risky for all of our running platforms.
In-house compiler, specifically made for the architecture that we sell, we have to maintain it but we rarely touch it. I am quite sure that it was related to System3, I know that we used to use System3 in the past. But this part I am not sure, as I am quite young and I personally never worked with it. Unfortunately it's not open-source, but it's just a standard compiler pretty much without optimization. In our case, it's the responsibility of the developer to write optimal code, since our compiler does help. I'd be curious to chat about your version of embedded Oberon, I will write to you !
I wasn't aware of them, most likely same path as Oberon microsystems which ended up creating Component Pascal, I was quite fond of, unfortunely didn't survive against Java and .NET.
Nowadays Oberon microsystems business has nothing to do with Oberon.
As proven by Go, those limitation don't matter when the language has an overlord to push it.
I was a big Oberon fan[0], not so much for Go, because in mid-90's using Native Oberon was an eye opening experience, in the hardware we had available back then.
20 years later (at Go 1.0 time), not so much.
[0] - The whole linage, and still think Active Oberon is the best dialect (with AOS) that eventually came to be, after Oberon v1.0 from 1987.
To me Go seems like almost a copycat of Oberon with channels added. But I neither used Oberon nor Go so that’s more of a spec-reading impression. What would you say having used both?
It is more like a Limbo copy cat, with a bit of Oberon influences.
As strange as it may sound, UNIX folks were Oberon fans, hence why ACME in Plan 9 had a similar developer workflow as the whole Oberon UI, where clickable text is combined with dynamic UI actions.
Plan 9 failed short of using a userspace systems language as C's successor (see Alef[0]).
Plan 9's sucessor, Inferno, fixed this with Limbo [1], where C is only used for the kernel, disVM, and a couple of userspace libraries, everything else done in Limbo.
Go ends up being a mix of Limbo, Oberon-2 method syntax and SYSTEM package.
However one thing that both Limbo and Oberon based systems have, and Go misses out, in how the whole platform embraces dynamic linking to extend existing applications, and surface operations to the UI.
By the way, I advise wasting a couple of hours diving into Inferno and Limbo's manuals[2],
Since your project is apparently about robots, how do you handle concurrency with Oberon? Or how can you avoid it? In case you're willing to share experience, you can contact me on me@rochus-keller.ch. (Btw: I'm currently working on a low-level version of Oberon without GC which can be used as C substitute.)
I'm curious if you have some references about this, if you have read good blog posts or something ! I can think of Rust that does not have exception, but it's not because they didn't address the problem of error handling: exception are replaced by `Result`, if I'm not wrong. In Oberon there's simply no error handling mechanism.
Can you really say Java succeeded in that sphere? It's simply not the case that Java applets had success. I was an early adopter, and was even paid to write a couple Java applets in '97, but I would say the whole thing was... questionable. They lingered about for a few years being badly supported and having a not-great security story, and then were unceremoniously killed.
The potential was there, but in reality people just used Flash, and when the DOM/JavaScript interface became more competent, they switched to using JS.
And, yeah, as others pointed out... Java did not have generics, or enums until 1.4, and its exception mechanism hasn't really stood the test of time.
Every time I have to track down exceptions in production on .NET projects, I deeply miss checked exceptions, because some people just code away without caring one second to check their errors.
That won't fly in code review in most Java shops I worked on.
Also the way modern languages that are worshiped for forcing to handle return values, with pattern matching, try and ? operators, it is nothing other than proving the point checked exceptions matter, even if they come with a different flavour.
I have been doing Java projects since 2006, and as mentioned on my edit you might have missed when replying, pattern matching, try and ? are just another way to package checked exceptions in a different coating.
Yes and no. There's a non-local-control aspect to exceptions that's a bit different from explicit return or ? in that it's explicit in the text of the program rather than in the signature of the method. But yes, the compiler will stop you in either case, so that's fine.
But there's no guarantee that someone under you didn't go and pull out a RuntimeException on you.
WASM uses a block structured IR (that is, no arbitrary gotos) and I would characterize it as in between of the [serialized] Juice AST and the flat Java bytecodes.
From the compiler's point of view the Juice AST is the same as source code so I'd expect the Juice JIT to have slight more code generation flexibility than the type-erased WASM.
I installed the Juice browser plug-in back then and remembered how Juice applets "loaded" much faster than their equivalent Java counterparts. Once loaded, Juice and Java applet performance was similar. Over time, Java benefited from a larger network effect and market share and I stopped seeing Juice applets on the web.
50 comments
[ 5.0 ms ] story [ 104 ms ] threadWow, that came out of the left field. Shipping a lexical compiler IR and letting the target do the code generation sound like a very powerful technique compared to compiling for virtual targets like WASM, JVM etc. Wonder why it didn't caught on.
Another intriguing tidbit:
> By the very definition of our tree-encoding scheme, it is impossible to encode a tree that violates scoping rules of our source language. Such an "illegal" program cannot even be constructed by hand.
This is somewhat similar to what V8 does with JIT compilation of javascript by now. I like the idea of doing it on AST rather than direct (heavier) source code. But maybe in the long run, gzipping the JS stream turned out to be just enough?
Today, with a lot of JS toolchains involving source-to-source transformation first, that could in theory achieve much of the same potential of modifying the original input, and lowering some things to a simpler subset etc., but just compression isn't the same.
The size reduction is pretty much a nice bonus with Juice rather than the point - the dictionary encoding matters because it means the decode gets similarity measures "for free" and can generated patchable code segments for partial subtrees that will be subsequently referenced.
I think there are two reasons it didn't catch on: The first obviously being Java, and the sheer amount of mindshare Sun was able to get for Java very quickly.
The other is that Oberon had gaps. It's a very austere language. And while all of those gaps can be plugged - Franz himself proposed extensions, as did other students of Wirth - without someone putting in the effort to create an Oberon version with sufficient expansions to appeal to business users, the way Sun did with Java, that the underlying technology was interesting didn't really matter.
I'm a big fan of the concept, but it's also more complex in some ways, even though a crude implementation is also potentially very simple. E.g. you "chop the compiler in two", and unlike a bytecode compiler you get a tree to work on. At the same time you can apply other JIT techniques - part of the work on SDE meant reconstituting the dictionary used to compress the tree on loading, and while doing so, if you generate code during the process, you can also directly reference the generated code to use as templates for a patching JIT and insert those templates directly into the dictionary.
"One day" I hope to get time to explore SDE more, because it seems JIT's have gotten so far down the bytecode path that we're missing out on seeing what else might be viable.
Franz own later research shifted a lot towards Java, but his students have interesting work in the JIT space too. Especially Andreas Gal, and his work on trace trees.
[1] https://en.wikipedia.org/wiki/Semantic_dictionary_encoding
At that point it's no different from with Java - if you add new bytecodes, the JVM needs to support it. Same here - if you add new node types, the code generator will need to support it.
Just as you wouldn't change the JVM bytecodes for every language change, you likewise wouldn't need to add new node types to support most language changes for Juice either. You could just as well choose to generate what in effect would be an intermediate representation instead. You'd only add new node types if they provide a sufficiently big win over generating more complex output, just as with the JVM
That said, nothing would've prevented dynamically updating the code generator.
[1] https://oberoncore.ru/_media/library/franz_m.code-generation...
One more is that, if you used an Oberon-based OS (eg A2 Bluebottle), the system and browser languages were the same. No need to learn separate languages. Integration will be smoother, too.
- No generic types. That's really limiting. You want to implement Linear Algebra for vectors ? You need a class for each possible types.
- No proper error handling : no such thing as `TRY` or `CATCH`. Instead, you need to pass a `BOOLEAN` mutable flag in functions that may crash. That's just sad.
- No distinction between a class, an abstract class and interfaces / pure abstract classes. If you want to write an abstract class, you just `HALT` in the base methods (like you would do in Python I guess). But there is no language semantic to define that this class is abstract. So the compiler can't help you out much with them.
- No enumerations. They are just very powerful.
(- Faulty garbage collector, even though that is not a a feature of the language but rather of the implementation that I am using.)
I am quite sure that Oberon was written with a real focus on designing a language for which it is easy to write a compiler / garbage collector. It really made sense back then. And they succeeded in doing so. However, for broad distribution of executable, it probably wasn't the best guess. These glimpses of the past are so interesting though, discovery of the most optimal tech stack was a long and dense path of exploration !
Juice also flows naturally from Franz PhD thesis, which was on JIT compilation of Oberon from what in some sense was a compressed serialized reduced form of the AST instead of bytecode [I'll stress this is a gross oversimplification - his compiler does do more processing, so I guess what is serialized is closer to an IR in tree form than the AST]. I don't know how much that changed in Juice, as I've never looked at it much but from the description it remains at least similar in concept. I've wished more work had gone into that ever since I read his PhD thesis in '94.
When compiling you had the option to generate proper native binaries, or slim ones to be JITed on load.
You seem to know a lot about this time. Would you have a reference on this report ? I am quite curious now !
(I use a similar mechanism of propagating vtables updates down the class hierarchy, but without the extra indirection - at cost of more memory - in my prototype Ruby compiler)
[0]: https://www.research-collection.ethz.ch/bitstream/handle/20....
really interesting that you work with something as niche as Oberon! :)
Can you talk about who is using Oberon, for what, and why?
Cool product, terrible boomer management.
Nowadays Oberon microsystems business has nothing to do with Oberon.
I was a big Oberon fan[0], not so much for Go, because in mid-90's using Native Oberon was an eye opening experience, in the hardware we had available back then.
20 years later (at Go 1.0 time), not so much.
[0] - The whole linage, and still think Active Oberon is the best dialect (with AOS) that eventually came to be, after Oberon v1.0 from 1987.
As strange as it may sound, UNIX folks were Oberon fans, hence why ACME in Plan 9 had a similar developer workflow as the whole Oberon UI, where clickable text is combined with dynamic UI actions.
Plan 9 failed short of using a userspace systems language as C's successor (see Alef[0]).
Plan 9's sucessor, Inferno, fixed this with Limbo [1], where C is only used for the kernel, disVM, and a couple of userspace libraries, everything else done in Limbo.
Go ends up being a mix of Limbo, Oberon-2 method syntax and SYSTEM package.
However one thing that both Limbo and Oberon based systems have, and Go misses out, in how the whole platform embraces dynamic linking to extend existing applications, and surface operations to the UI.
By the way, I advise wasting a couple of hours diving into Inferno and Limbo's manuals[2],
[0] - https://en.wikipedia.org/wiki/Alef_(programming_language)
[1] - https://www.vitanuova.com/inferno/limbo.html
[2] - https://www.vitanuova.com/inferno/docs.html
The thing that made Java great were conventions (file per class etc) and its standard library (esp collections)
Unsurprisingly, that is exactly what made Ruby great
Have a look at this version of Oberon which has generic modules, exceptions, enumerations and much more: https://oberon-lang.github.io/
Also a concurrency concept is work in progress: https://github.com/oberon-lang/oberon-lang.github.io/blob/ma...
Also the abstract class thing didn't stop C++, lol
The potential was there, but in reality people just used Flash, and when the DOM/JavaScript interface became more competent, they switched to using JS.
And, yeah, as others pointed out... Java did not have generics, or enums until 1.4, and its exception mechanism hasn't really stood the test of time.
Also the way modern languages that are worshiped for forcing to handle return values, with pattern matching, try and ? operators, it is nothing other than proving the point checked exceptions matter, even if they come with a different flavour.
But it was the 2010s.
These days I work in languages with better static guarantees. Always wanted that back then, but felt like a minority.
But there's no guarantee that someone under you didn't go and pull out a RuntimeException on you.
https://chromewebstore.google.com/detail/cheerpj-applet-runn...
From the compiler's point of view the Juice AST is the same as source code so I'd expect the Juice JIT to have slight more code generation flexibility than the type-erased WASM.