Replies: 6 comments 4 replies
-
Regarding more comprehensive component model, there are still a few things that I'm not sure are possible (e.g., one is how to handle component identification as I mentioned in 0xPolygonMiden/compiler#77 (comment)), but I wanted to think through a more narrow problem: static data segments. I'm assuming we want to achieve the following properties for these:
The above functionality is not something that Miden VM (or Miden assembly) supports out of the box, but we may be able to enable it via a combination of conventions, new instructions, and maybe even new overall concepts. Below, I'll start with the simplest case, and then will expand from there. Single-context executable programThe simplest case is: an executable program which does not use any
This approach should be relatively efficient: we can initialize the data at a rate of ~16 bytes per cycle (so, initializing 16KB should take about 1,000 cycles). We also do this only once - and this is really the best we can hope for. The main question though is where to put this static data. One approach is to store it starting at We could introduce some new syntax to MASM to improve ergonomics of handling such static data.
In the above, the assembler would inject a program right after Library modulesThe above approach works very well for executable files (in fact, I'm not sure we can do much better in terms of efficiency), but it doesn't quite work for library modules. Consider the following module:
There are two challenges with this:
One option is to write the data to memory any time either One way to improve on this is to make the initializer program a bit more sophisticated and put some metadata into the static memory section. For example, we could say that memory address
This initializer would be run at the beginning of both The main question here is how do we perform the check in step 1 above. There are a couple of options here, but I still need to think through their pros and cons. Multiple execution contextsThe above approach should work well as long as we don't use One potential way to get around this is to always put static memory segment into the root context. That is, the initializer would always be invoked via a Another downside is that "unhashing" segments generically (which we'd need to do if we want to write static segments into the root context), would reduce efficiency of initialization (e.g., it would probably at least double the number of cycles required to initialize a given amount of memory). So, while this approach is probably acceptable - it's not without its tradeoffs, and I'll try to think about alternative approaches. |
Beta Was this translation helpful? Give feedback.
-
From 0xPolygonMiden/compiler#120 (comment)
This was just a "guesstimate" based on how much work I think a verifier may be able to do without impacting recursive proof verifications too much. Assuming each element contains 4 bytes of data, 4KB means we can initialize memory with about 1K elements. For each element, we'll need to 2 additions, 1 multiplication (in the extension field), and probably hashing as well. Overall (and this is a guess at this point), I hope that using some of the specialized instructions we have in the VM we can do all of this in under 5K cycles which may be on the border of what is acceptable (or overall target for recursive proof verification is ~65K cycles). One thing to clarify: this 4KB limit is what we can do for the entire program (not for a given component). I also think that trying to support both approaches (at least from the start) - will probably add too much complexity. We can assume that for now we go with non-deterministic unhashing approach, and we can come back and evaluate this approach later on as an optimization. One thing we should probably get a better handle on sooner rather than later is how much rodata do we think may be needed for "typical" smart contract. Here, defining what is typical will be tricky - but maybe we can make some assumptions (e.g., use of static strings will be minimal or absent altogether). I think knowing this will help us prune out some solution approaches.
Yep, understood that this is not how most compilers work, and we probably don't need per-function rodata right now. This is however, where we probably want to end up because otherwise there will be a natural limit on how complex smart contracts on Miden could get (i.e., a single function which requires lots of rodata would slow down every other function - even if they are not related). So, probably at HIR and assembly levels, but especially at the VM level, we should go with the implementation which supports per-function rodata. One thing I'd like to understand is how this is handled in the component model. Wouldn't each component get its own separate rodata? And if so, could we make each function of the account interface its own component? |
Beta Was this translation helpful? Give feedback.
-
Comments for 0xPolygonMiden/compiler#119 (review) First, I think the overall assessment in the above note is correct: we don't know yet if we can support the component model fully (or rather how much work it will take to get there, and whether the tradeoffs make sense). My understanding was that for now we were using the component model primarily as a way of describing the ABI (instead of coming up with our own ABI format) rather than going all the way to assuming full functionality of the component model. But maybe these are more inter-related than I thought? Second, before getting into the details about supporting component model within the VM, I think it may be worth aligning on the priorities. In my mind, the main thing we need to achieve sooner rather than later is to be able to compile programs written in Rust targeted specifically for the Miden rollup. We, of course, want to support more general things - but these can come later. Concretely, the above means:
This is not to say that we shouldn't work toward supporting full component model in the VM, but I also don't want to solve a bigger problem than what we currently have to. Onto more specific comments:
This is not technically correct. All contexts are persistent - it is just there are no instructions available to access them. We could very easily add instructions which, given a context ID, could read or write memory of any of the previously created context (or even, using non-determinism, we may be able to write into future contexts as well).
This may require a pretty fundamental change to the VM. Currently, a context is defined solely by its memory space (i.e., the only thing that is different between two contexts is the memory they can access), however, it seems like here a context is defined by its resource table - right? Another fundamental change is how we authenticate which procedures are allowed to execute in a given context. As you noted, the VM currently does not do any authentication at all. In the context of the Miden rollup, it is the kernel who is responsible for authenticating procedures. Basically, in the current model the VM + the kernel work together to define resources and who is allowed to access them. Or said another way, the resources live in the kernel memory and it is the kernel who creates a semblance of a component model for the Miden rollup. In the proposed model, we'd need shift this responsibility to the VM. Again, I'm fully onboard that this is the right way to do it long term (and in fact, when I was designing the VM I wanted to do something like this - but ran into some limitations on the ZK side; these limitations may be lifted by the GKR work discussed in #1182), but I also would like to avoid solving a bigger problem then we currently have to.
For what we need now, the only time we need to do a context switch is when a note calls an account interface procedure - everything else could be abstracted away from the compiler. For example, the compiler would not need to emit any
I'd like to understand this point better (and maybe we can discuss it live) because, in the context of note-to-account calls, I think the solution could be relatively simple. But it is possible that I'm missing something.
There are no limitations on how deep in the call stack a call to a kernel procedure can happen. But also, for what we need now, the compiler may not need to emit explicit calls to the kernel at all (though, again, this may be something to discuss). |
Beta Was this translation helpful? Give feedback.
-
Basically, I think the initial outcome I arrived at was that we could rely on the Wasm tooling to solve the following:
What seems increasingly apparent to me, is that we have been trying to make local/isolated decisions about various concerns (e.g. what the function call ABI should look like, how is memory initialized, how are modules linked/dependencies managed, how are they packaged, what is in a package, how is it published, etc.), when in fact a decision on any one of these matters impacts everything else in subtle ways. Finding the right set of primitives could provide solutions (and/or a clear path) for all of these, and likely things we haven't yet considered. I keep coming back to the Wasm CM for two reasons: 1.) it provides a unifying framework for virtually all of the topics I mentioned above, and elegant solutions to them; and 2.) the degree of overlap it has with our domain, and the set of problems we're working to solve (and will need to solve at some point). To recap the main benefits:
I agree, though I would caveat my agreement on what constitutes "general" here - I'm definitely a fan of hacking things together to make something work when I know replacing it later won't be a huge deal. That said, a lot of what we're talking about here are so foundational that trying to change them later will not be a small effort, where (depending on the effort involved) solving them properly now will let us move faster across the board. If we decide to go with a temporary solution, it may not be possible to avoid breaking users of Miden later when we need to replace that with a proper solution, is that going to be an acceptable tradeoff? I guess what I'm getting at is, in the spectrum of tradeoffs between speed and technical debt, we really want to make sure that the "interest" on that debt doesn't drown us.
I think we essentially get this "for free" just by using Wasm and the CM on the frontend of the compiler, to a certain extent. One thing to note though, is that interop between Rust and Miden Assembly is also part of the cross-language compatibility picture, so we can't fully kick that can down the road.
I believe we can probably do this, and in fact was the "temporary" solution @greenhat and I discussed today in our call. That said, there are still some aspects of this that could become a problem. I'm in particular thinking of how Supporting context switches between arbitrary contexts is the only obstacle to being able to implement CM resources, which would provide a richer primitive in Miden for such things. There would be additional work related to resource management itself, but in comparison to context switching it is much simpler and straightforward. With both context switches and resources, we have everything we actually need from the CM, and everything else is completely optional from my perspective.
For sure. I do want to be more clear that the "full" CM is really not what we're after, it really is just some of the more basic primitives/semantics. That unlocks everything I think we'd need in practice. But yeah, if we can get away with less, I'm cool with that. I just want to make sure that we're actually getting away with it. If you'll permit me a metaphor: we're playing a game of Jenga with all of these pieces, trying to find which ones we can remove and put on top of the tower to deal with later, without bringing everything down. Later when we have to rewrite some part of the system "properly", it's basically pulling that piece back out, and trying to put it back in its original place. My worry is that if we get too eager to remove things at this stage of the game, we may find ourselves in a position where we can't pull any more pieces without bringing the whole tower down. There's no way to know for sure though if that'll happen, so that's why I'm hoping to quantify what the actual choices are we're making (in terms of actual effort, time to implement/deliver, knock-on effects, etc.). It is also possible we may have to actually do some portion of the work just to be able to do that much, e.g. I don't know how else we can quantify the problem of arbitrary context switches without you (or someone with your knowledge) actually figuring out (at least in theory) what the possibilities are for representing that in the constraint system. From there I think we can work things out more easily, but that's still (to me) the biggest unknown, and makes evaluating the different paths here hard to do.
I'm going to follow up on these in a separate comment to avoid this one getting too long. I shouldn't be too long, maybe an hour or two. Footnotes
|
Beta Was this translation helpful? Give feedback.
-
Makes sense, though I think in terms of semantics, the notion that a context is transient is effectively true, even though technically the contexts are never cleaned up until after the program terminates. Once a call returns, the context it executed in is inaccessible. The distinction is only important in terms of support - since contexts aren't in practice persistent today, that gives us flexibility in terms of how we implement changes to context behavior. In any case, I think ideally we would keep contexts an internal implementation detail of the VM, corresponding essentially to a CM component "instance", and managing the context switching implicitly based on some semantics we define (e.g.
To be clear, a context would become something more than just an index/identifier for the linear memory in use; but in the CM, there are two bits of "context:
So I think it's probably more accurate to say that there are really two bits of state here that have slightly different lifetimes. However, there is flexibility in how this state is managed, what the limits are (maximum resources, borrows, etc.) to accommodate implementors. The only thing that actually matters is that the semantics are enforced (e.g. around borrowing), how it's actually done is completely up to implementors. Just to elaborate a bit on what constitutes a "resource" - in Wasm CM semantics, the raw representation of a resource handle is an "opaque address" in the form of a u32 (though that representation is up to the implementor); that representation is not actually accessible from Wasm, but for the Wasm runtime, it is presumed to be an index into the resource table of the owning component instance. An entry in the resource table consists of the following bits of information: the unique identifier of the resource handle, the type id of the resource, whether the resource handle is owned or borrowed, the number of "lends" (if owned), the number of "borrows" (if borrowed), and the index of the destructor function for the resource (if one is defined). There are two synthetic functions implemented by the runtime, and exposed to the component instance defining the resource
My understanding though is that the current authentication is pretty brittle due to the limitations of what we can do with the tools we have, and as a result, doesn't allow for future growth to support more subtle composition of notes/accounts, e.g. delegating some action to another note/account/etc. on your behalf using a restricted capability that permits just that action to be taken and nothing else. That might not even be something we care about though. I suppose the relevant question right now is really about how brittle the current mechanism is, and whether it will present an issue due to the limits on how much control we have over compiled programs. As I seem to recall the current mechanism assumes that the call stack is of a particular depth (or something to that effect, it's been quite awhile since we discussed it, and I can't remember if it was call stack depth, MAST depth, or something else). As I mentioned in my previous comment, confirming those details and whether or not it will be a problem will be useful for weighing our options here.
Makes sense, I agree that if we can punt on resources initially, it would be preferable, even though it would give us more options in terms of defining stuff in Rust, I don't think that's nearly as important right now as context switching to be honest. Having context switches available would at least make resources an option in the future.
If we punt on cross-language compatibility, and resources, then agreed, the only other "calls" we have are from notes to accounts, and potentially syscalls (there is no difference between Having the compiler handle rote mechanical details (such as canonical ABI lowering/lifting) is what compilers are good at, and we should lean on it for those sort of things. What we want to "abstract away" from it though, are details which are best left up to the VM (for example, how memory is initialized). If expose those internals so the compiler can coordinate with the VM on something, then any change to the VM implementation (or vice versa) is a breaking change; whereas if the VM handles the details of e.g., initializing memory (with a higher level "public" interface for the compiler), then we can changes much more freely later on. Some brittleness at this stage is fine though, so I'm not saying we can't have the compiler and VM know each other's implementation details, but in terms of deciding which things we want to provide "primitives" for, that's the criteria I would use. The main problem of course is memory initialization at call boundaries, once we figure out our solution there, we at least have a path forward.
To be clear I was referring to the fact that if we didn't use the Wasm CM tooling/ABI/etc. at all, we'd find ourselves having to reimplement a bunch of it anyway, simply because Rust as a language assumes a shared-everything model, where all code being executed is in the same address space. This means that Rust might make codegen decisions based on that assumption which would produce unsoundness if those assumptions are violated. One of the things the CM gives us, is Rust support for a shared-nothing model when calling across components. To reiterate from earlier, we can restrict our use of the CM for just note scripts/accounts to provide us that useful property, but then manage everything else using the standard Rust dependency model. It restricts us somewhat in what we can express, but we can ship something, which is the critical bit.
Ah ok, this is one of the answers to the questions I had. I could've sworn there was some other hardcoded thing in there, but I must be misremembering. I think the main thing for me that I would like to quantify is:
After discussing everything tomorrow, and reviewing the data for the above (to the extent we can come up with some), it'll be a lot clearer what our options and tradeoffs are. I know I'll certainly feel a lot better about where we land if we have some data backing us up, even if where we land is "not implementing any of this". |
Beta Was this translation helpful? Give feedback.
-
A quick note on how we can use the advice provider to move large amounts of data across To set the context: when we execute the Similarly, when the callee returns, it is also able to use only the top 16 elements of the stack (in fact, the callee must ensure that it doesn't have more than 16 elements on the stack on return). And so here too we need to use the advice provider to transfer large amounts of data. The process for doing consists of the following steps:
To compute the hash of some region of memory, we can rely on mem_stream instruction. This instruction will read 2 words (8 elements) from memory starting at the specified address (address is in the 12th slot from the top of the stack), and will also increment the address by 2 (so that it points to the next 2 word) - all of this is done in 1 cycle. Another instruction we need is hperm. This instruction executes a single permutation of the RPO hash function. Here is an example of how these would come together to compute hash 16 words starting at some memory address
Though, we probably don't want to drop the pointer at the end because for the next step, we'd use adv.insert_mem instruction. This instruction will create an entry in the advice map using the word at the top of the stack as the key. The memory region is defined by the elements following the key. For example:
After this, we can execute the
|
Beta Was this translation helpful? Give feedback.
-
This document describes an initial draft of a plan for how to bring the key features of the WebAssembly (Wasm) Component Model to the Miden toolchain, and by doing so, solve a number of complex problems that we are either currently facing, or have not even considered yet. Current plans (regardless of this proposal) are for the Rust toolchain, currently in the initial stages of development, as well as the Wasm frontend of the compiler, to produce/consume the "real" Wasm component model, for a variety of reasons. However, the backend of the compiler, and the rest of the Miden toolchain would, if this proposal is accepted (in whole or part), adopt a Miden component model. This would be based on the Wasm component model, but modified to the needs and unique constraints of the Miden VM.
NOTE: This is more of an umbrella proposal, it's not all-or-nothing, and it may be that some elements of this aren't a good fit and others are. I'd like to keep the overall ideas intact, and am less concerned about specific implementation details, but definitely raise any questions/issues you have. I'm only passing familiar with some of the VM internals, and even though I've tried to at least ensure various parts of this proposal are in theory possible to implement, there are probably many things I haven't thought of, and some which I may have considered, but missed something key when digging around.
Lastly, the proposal here is intended to be implemented in phases, and without causing any significant disruption to our roadmap. My intent would be to have the compiler team take on the bulk of the work, since the majority of it would be in the compiler and assembler, but a few of the proposed changes would require working closely with the VM team to implement.
Background
This proposal came about over the last week as Denys and I have been working through the details of what is currently called the "Miden SDK", i.e. the Rust frontend for Miden. This SDK consists of two parts:
Compiling a Rust program to Miden Assembly involves both
rustc
andmidenc
, by first generating a Wasm module viarustc
, and then compiling that to Miden Assembly viamidenc
.That all sounds relatively straightforward, but there are a number of rather sticky challenges hiding here:
call
instruction into the caller. We have to come up with clever ways to protect against that sort of thing ourselves with proc macros and such, which quickly becomes a mess.extern
declarations and such, but this is easy to get wrong, especially for end users, and the only reasonable solution is, again, proc macros.i32
ori64
. This prevents us from being able to handle calling conventions for these modules inmidenc
, and also results in worse code generation, because we are unable to elide code required for type assertions in many cases.midenc
. Hand-written MASM functions currently use an ad-hoc ABI, at best, and other compiler projects are likely to use their own in lieu of something canonical. This makes interop painful, if not outright impossible in some cases.A-Za-z0-9_
, and there are plenty of languages that use kebab-case symbols, or allow symbols like!
or?
. This is obviously fixable, but is an example of an area where assumptions were made in MASM that really only hold for hand-written MASM. Aside from removing the limit on length, we could allow a much broader set of characters in symbol names by using quoted identifiers for those that don't adhere to the limited set used by hand-written MASM, e.g.exec."some-module::is-valid?"
. But that's a separate discussion.There are likely a few other things that I'm forgetting to mention at the moment, but I think in general the problems as I see it can be categorized in three major ways:
Towards the end of the week last week, Denys and I started investigating how we might solve some of the frontend-specific issues, as well as some of the future roadmap issues, via the Wasm Component Model. It quickly became apparent that not only would it address some of our immediate blockers, but could also be applied to a number of these other issues.
Component Model?
I'm sure by now, you've either checked out the link I dropped at the top, or been going crazy waiting for me to explain what the hell a component model is, and what it has to do with anything I've been talking about so far. This section is for you! Hopefully this will provide enough context to orient you, without drowning you in details, but I'm happy to answer questions directly too.
NOTE: I'm going to describe this in terms of WebAssembly, because I want to avoid conflating the current state of Miden Assembly (and the VM) with the terminology here. However, I will describe how all of this maps to Miden Assembly in later sections, so consider this section as providing essential context: what exactly is the Component Model, what problems it solves, etc. Our own implementation will likely differ in various ways, but by understanding how the Component Model was introduced to the WebAssembly ecosystem, it will better illustrate how we do so for Miden.
WebAssembly Refresher
WebAssembly (wasm32) is a Harvard-architecture machine (i.e., like Miden, code and data are in separate address spaces, unlike a Von-Neumann architecture in which they are in the same address space). Also like Miden, it is a stack machine, with a relatively tiny instruction set. It is a 32-bit machine (though a wasm64 variant is in the works), but with a native 64-bit integer type. Memory is byte-addressable, and represented as a flat, linear address space, limited to 4GB of memory. It was originally designed for use in web browsers (hence the name), but has since evolved for use cases outside the web. It is used for edge computing, sandboxing, as an orchestration runtime (e.g. Kubernetes can run Wasm programs natively as if they were containers), and has seen use in blockchain contexts as well. For example, NEAR uses Wasm to run its smart contracts, and has even been exploring the Component Model to take advantage of the benefits I'll be talking about.
NOTE: Below, I'll refer to "core" WebAssembly, which is to say the parts of WebAssembly specified in the MVP. The Component Model proposal extends core Wasm in various ways.
Wasm, aside from the abstract machine itself, also specifies the concept of a module, which can be expressed in text form (
.wat
), or in binary form (.wasm
). A module contains the following items:funcref
table, which holds typed function references, is used by thecall.indirect
instruction - a function "pointer" is simply an index into afuncref
table, and if the index is invalid, or the call doesn't match the signature of thefuncref
, execution traps)main
, but is instead used for things like global constructors which must be run before a program starts.So, when you compile a Rust program to Wasm, what you get is a Wasm core module as described above, in binary form.
The Component Model
Unfortunately, core Wasm only has a very limited set of types to express things with (integers, floats, and opaque reference types like
funcref
). When compiling a high-level language to Wasm, you take high-level types like records (structs), strings, etc., and translate their representation into this limited set of primitive types in a language-specific way, losing the high-level type information in the process.In order to interoperate between languages, both sides must agree on this low-level representation, and this contract is called an application binary interface, or ABI. For example, a Rust string and a JavaScript string will likely have completely different representations, so to allow JS code to call a function implemented in Rust, with a JS string as argument, either the Rust code must know the precise details of the JS string layout; the JS code must convert to the low-level representation expected by Rust; or, they must agree on some common representation.
This is obviously not ideal. The Rust-compiled module must explicitly export it's functionality using some lowest-common denominator representation in order to maximize portability; or alternatively, commit to a less portable, but more optimal representation that is only supported by specific languages/compilers. Not only that, but in both cases, this is a really fragile arrangement - changes (intentional or not) to the low-level representation could break in unexpected ways, with no way to know that the contract has been broken.
One of the key goals of the Component Model is to solve this problem. It does so by first expressing type definitions written in an interface description language (IDL) called WebAssembly Interface Types, or WIT. A component author describes the interfaces they wish to make public using WIT, using rich types (including things like strings and structs). Both the component author, and the downstream users of the component can generate bindings from the WIT description. The specification which describes how the rich WIT types are translated to core Wasm primitive types is called the Canonical ABI. This ABI defines not only the layout of the WIT types in terms of core Wasm types, but also the calling convention for component functions. Thus, a Rust-compiled module can provide a Rust-native API, express it in terms of WIT, and then generate Canonical ABI bindings for it that can be called from any language that also compiles to the Component Model.
Fundamentally, a component in the Component Model is a wrapper around a core module, that specifies its imports and exports in terms of WIT interfaces. Components are linked and "instantiated" at runtime. These component instances are a bit like OS processes spawned from an executable; the executable is stateless, and simply describes the contents of the program. Once spawned however, the process is stateful, it has specific resources allocated to it, and adheres to the normal process lifecycle. Component instances are very similar - the host runtime acts a bit like an OS kernel, and instantiating a component is equivalent to spawning a process; the host runtime will allocate whatever resources are needed to run the component, and then orchestrate its lifecycle, along with all of the other component instances which are active. Inter-component calls involve context switches between the instance of the caller and the instance of the callee, while intra-component calls all share the same context.
WIT interfaces are the means by which components can be easily reused across languages. You can call a Go function directly from Rust, and vice versa, without either side having to know anything about what language the component was written in. All that is required is the component interface, expressed in WIT.
Furthermore, components are designed to be linked together dynamically. If a component imports the
wasi:http/handler
interface, any component that implements that interface can fulfill that dependency, providing an incredible degree of modularity at runtime. This can be done ahead-of-time as well, as multiple components can be linked together into a larger component with fewer dependencies, and shipped as a unit.A key design goal of the Component Model is to build on Wasm's sandboxing model, which is much like Miden. Components are designed for a shared-nothing architecture - component instances fully encapsulate their linear memories, tables, globals and other resources. Component interfaces only allow value types (no passing arguments by reference, as there is no pointer type), opaque typed handles (i.e. resources, and immutable uninstantiated modules/components. While handles and imports can be used as an indirect form of sharing, the dependency use cases documentation describes how this sharing can be finely controlled. Components are even more restrictive than core Wasm modules in some ways; for example, components may not export Wasm memory. This not only provides stronger sandboxing guarantees, but ensures that different components with different assumptions about memory can interoperate without issue.
One such example of how sharing can be precisely controlled, is link-time virtualization. In short, this allows, at link-time, an instance of a component to be created which replaces one or more interfaces imported by its children, with a "virtualized" instance of that interface. This does two things: 1.) prevents the children from having direct access to the "real" interface, and 2.) allows the parent to intercept calls to the interface and modify its behavior. This can be used for sandboxing use cases, instrumentation, etc.
All of this is described in the binary form of a component module, and allows a host runtime to analyze the component graph and reason about its properties and behavior before running the component.
Goals
So, to recap, the following are the primary goals of the Component Model which are relevant to Miden:
Non-Goals
To make sure it's clear what the Component Model does not aim to solve on its own:
Proposed Changes
So we've spent a bunch of time on set up, now let's look at some concrete details about what this proposal entails! The following are the major bullet points:
call
'd vsexec
'd based on component interfacescall
boundaryuse
), at least initially. What would be new, is that MASM libraries/programs would be required to describe their public interface using WIT. To produce a final binary for the VM, the assembler would perform a linking step over the set of components provided, and if successful, would produce the final MAST, as well as a component manifest that describes the assembled component tree, along with the MAST roots associated with each interface. In binary form, this would be suitable for deployment as well as introspection by analysis tools.call
instruction, the hash of the caller would be stored immutably in the instance state, in order to support thecaller
instruction from code within that instance.exec
instruction, it would handle context switches if the callee is in a different component instance than the caller. If the callee instance does not yet exist, it would be instantiated at this time.call
instruction, it will always create a new component instance for the callee, and set the MAST root of the caller as thecaller
for that instance. When acall
returns, the instance would be scheduled for destruction on another thread (to avoid the main thread of execution having to incur the overhead of freeing resources associated with the instance).Differences between Miden and Wasm Components
Rather than fully specify the Miden component model here, I think it is easier to specify the ways in which I expect it to differ from the Wasm model:
(import "mast:0x..." ..)
. We can then special-case imports from that namespace in our linker implementation.midenc
, and some of it is not yet specified anywhere (spilling via the advice provider).call
vsexec
interfaces in WIT. A simple approach we're exploring in the short term is using marker attributes in doc comments in the WIT file itself - these are preserved in the binary encoding of WIT, and can be used to drive custom behavior in code generators as well as other tools such asmidenc
.Benefits
In addition to solving a number of the problems I've laid out earlier in this document, I believe this proposal also has additional benefits that are worth considering/keeping in mind:
Drawbacks
I think any good proposal needs to test its mettle by explaining why it might be a bad idea, so here goes:
I hope you found this useful, and at the very least thought-provoking! I'd really appreciate your feedback, and in particular I'd like to hear from the assembler/VM team with your initial thoughts. I'm available to discuss anytime!
Beta Was this translation helpful? Give feedback.
All reactions