Closed akkartik closed 1 year ago
I'm obviously not Maxime, so I can't speak for her, but I do have some thoughts on this.
If the plan is to embed UVM and drive different computer systems(think a 6502 in a c64, and a 6502 in a NES), then a syscall makes sense. That way it creates a boundary between the CPU and the computer interface. Whereas if you bake the mouse into the CPU instructions, it creates overhead for implementations in systems where a mouse is not a peripheral.
There's a few reasons why I went with a syscall
instruction.
At the moment, there are less than 80 opcodes, that makes it possible to encode all opcode into a single byte. The idea being that the most commonly used instructions would be just one byte long. This might seem trivial, but the size of the generated code does matter for transmission over a network, for example, and for cache performance. In general, the opcode space is fairly precious if we care about optimizing for size. We can have a special opcode that is an "extended length instruction", but that adds a little bit of complexity to the decoding.
The other thing is, at the moment, the syscalls are organized into different subsystems (window, audio, network, etc). This is in part done because eventually I want to have a permission model for different operations. For example, by default, programs running on UVM won't have read/write access to your filesystem. You'll have to explicitly grant permission. The reason this is done is so that UVM can eventually have something like an app store (or app catalog), where you can safely run programs you find on said app store. If we just stuff all the syscalls into the opcode space, that doesn't really work anymore.
A third reason is that, generally speaking, the interpreter (and eventual JIT) will be optimized to run instructions fast. Syscalls, however, typically represent more complex operations that have to call into a function to run. So there's kind of a distinction between primitive operations you can expect to run fast, and more complex, potentially more expensive operations that may also require special permissions.
Then, there's the question of supported features and evolution. Kind of what @neauoire was hinting at. Not every system is necessarily going to support all UVM features. UVM will guarantee that all opcodes are always available, but some syscalls may not be available on some machines. For example, on a machine with no display, you may not be able to create a window.
There's also the question of evolution. I'd like to more or less freeze the instruction set, but I expect that the amount of functionality supported by UVM will grow over time. I'd like to have a MIDI subsystem eventually so we can connect to MIDI device. That subsystem doesn't exist yet though because it's not a high priority at this time. It will be easy to add a new subsystem for it later though.
Thanks both! I better understand the design goals here.
Having to support multiple devices makes a lot of sense. There's a space here between "write once, run anywhere" and "write once for device X, and it'll always work for device X." The variable getting modulated is how much fragmentation across devices you're willing to tolerate. Your blog post at https://pointersgonewild.com/2023/02/24/building-a-minimalistic-virtual-machine is pointing out that we live in a world of extreme fragmentation where it's very difficult to tell if two computing stacks expose the same interfaces. And the players in this world don't have an incentive to combat fragmentation. Your answer helps clarify that you're not aiming all the way at the other end at the spectrum, but perhaps somewhere near uxn.
From my perspective the other reasons feel like implementation details that users of the VM won't care about. It's easy to set aside a contiguous range of opcodes for screen, memory, etc. The VM can still choose to manage tables of function pointers for such ranges of opcodes, permission them differently, ignore them for JIT'ing purposes and so on.
I think controlling binary size is a solved problem in UVM since you're open to variable-length opcodes. And even that seems far away. You've used 80 opcodes, you have 176 single-byte opcodes left. So there's a lot of room for maneuver yet. If you got to 256 opcodes along with a similar number of syscalls, it becomes harder to make a case that this is a minimalistic VM.
Like I said, I better understand your reasons. I'm happy to close this issue.
There's a space here between "write once, run anywhere" and "write once for device X, and it'll always work for device X."
Yeah. Realistically, at the moment, UVM targets mostly laptops and desktops. If the project succeeds, we'll probably add touch APIs as well to support tablets and cellphones. My expectation is that most devices would support the core functionality of window/mouse/audio APIs. But obviously, some software can't run anywhere. For example, if your software needs 16GB of RAM to run or very fast compute for graphics, it won't run well on an older machine.
It's easy to set aside a contiguous range of opcodes for screen, memory, etc.
Sure, but IMO it's also like... What does that gain you? What do you really gain by saying "everything is an opcode"? UVM. has a syscall instruction, and the implementation is still very small. If everything is an opcode and I want to have a permission system, then I need to have logic to manage permissions on a per-opcode basis. Does that really simplify anything?
To me, it feels a bit like the Smalltalk idea that "everything is an object". Having implemented a VM for JavaScript and Ruby, I can tell you that's BS. The VM has to play all kinds of games and there's all kinds of complexity hidden the surface to try to maintain the charade that everything is an object, but no, integers are not objects, they're a core primitive, you have to give them special treatment if you have any hope of having decent performance.
I think controlling binary size is a solved problem in UVM since you're open to variable-length opcodes. And even that seems far away. You've used 80 opcodes, you have 176 single-byte opcodes left. So there's a lot of room for maneuver yet. If you got to 256 opcodes along with a similar number of syscalls, it becomes harder to make a case that this is a minimalistic VM.
Yeah I think that I'll probably have ~110 opcodes once I have 32-bit integer support and 32-bit float support more fleshed out. Leaves some room for 64-bit floats eventually and a few more optimized opcodes.
In addition to variable-length opcodes, I'm thinking it could also make sense to support gzipped images (or some other kind of compression, e.g. have the VM be able to run myprogram.uvm.gz
files directly.
68k Macintosh would execute an unimplemented or illegal instruction to do a "syscall" (or "A-trap," as they called them) by causing a fault, and the OS would use the fault handler interrupt it had already set up to figure out what code to run.
https://en.wikipedia.org/wiki/Macintosh_Toolbox#On_68k_systems
68k Macintosh would execute an unimplemented or illegal instruction to do a "syscall" (or "A-trap," as they called them) by causing a fault, and the OS would use the fault handler interrupt it had already set up to figure out what code to run.
https://en.wikipedia.org/wiki/Macintosh_Toolbox#On_68k_systems
Cool trick :)
It's easy to set aside a contiguous range of opcodes for screen, memory, etc.
Sure, but IMO it's also like... What does that gain you? What do you really gain by saying "everything is an opcode"?
Just to answer this, my idea is that a single namespace imposes some additional self-discipline to keep things minimalist over time.
Basically, I look at past platforms and see a bunch of smart people with good intentions. The fact that they fell into this trap suggests that it's a very easy trap to fall into. So I tend to look for external help in resisting temptations.
I'm curious if you considered making each syscall a first-class opcode.
What I've observed is that every new namespace gives you more room to add features. If the goal is stability, it might be useful to eliminate namespaces as far as possible to minimize temptations to add features.
A single namespace of opcodes might be easier for people to learn.
Real processors sometimes introduce a 'syscall' instruction for ease of implementation, but that seems irrelevant for a VM.
I'm curious if there are other considerations here I haven't thought of.