Designing a non-Haskell DSL that compiles to Plutus-Core

Hello everyone,

For a while now I’ve been considering designing a non-Haskell Domain-Specific-Language that compiles to Plutus-Core.

This could benefit the Cardano ecosystem significantly as it could be designed to be much more accessible than Plutus.

I think there are two important requirements for this DSL:

  1. it should be readable by the majority of programmers, regardless of their background
  2. its compiler should be written in Javascript (or Rust/Wasm), so it can be used in IPFS-hosted DApps

Is anybody else here interested in such a DSL?

3 Likes

This could be broken up into multiple requirements:

  • DSL output should be compatible with pinning (IPFS, Arweave, etc)
  • DSL should be compiled and verified (PAB, plutus playground, etc)
  • DSL spec should be open to implementation (typescript, python, etc)

Related Reference: https://forum.cardano.org/t/how-exactly-does-plutus-core-access-the-datum-redeemer-and-scriptcontext

1 Like

Just to be clear: I would write DSL code like how kernels are written in OpenGL/OpenCL.

The DSL code would reside in a big literal string, or optionally a separate file, and a Javascript library would help you compile that into Plutus-core.

At a later stage libraries to process this literal string could be created for other languages.

It might feel foreign to program inside a literal string, but, once you configure your IDE syntax highlighting to recognize the DSL code, it’s not a bad experience.

I wouldn’t try to transpile existing languages directly into the DSL/Plutus-core. That’s basically what Plutus does with Haskell, and given that design decision IOG did an excellent job. I don’t think a community effort could achieve anything equivalent.

Analogy with GPGPU
For my work I sometimes have to write GPGPU kernels to accelerate calculations.
I used to do this in Nvidia’s CUDA. CUDA is basically a dialect of C, and Nvidia created a special compiler to convert CUDA code into two instruction sets: one for the host CPU and one for the GPU. This is analogous to Plutus’ ability to create off-chain and on-chain instructions from the same source.

Later I switched to OpenCL for GPGPU programming. The device instructions are created at runtime by compiling OpenCL code that lives in a string. The OpenCL code can’t be used for host-side calculations, but now my GPGPU code is vastly more reusable due to the custom code generation possibilities, and due to multi-language support. For me these advantages far outweigh the disadvantages.

What I’m proposing here is analogous to how OpenCL generates GPGPU kernels, and I think it could be a competitive alternative to Plutus.

3 Likes

That’s actually a fairly excellent analogy.

OpenGL is the API and OpenCL is the DSL. However as you know there are many implementations of both available. A good DSL should be expressive for the domain it is specifically for regardless of implementation but without something like Mesa would this be easier than learning Plutus?

1 Like

I don’t understand why you mention Mesa. The blockchain itself is the ‘driver’ in our case, no?

We just need to provide a compiler. The off-chain parts can be done with existing tools (cardano-cli, Blockfrost etc.)

would this be easier than learning Plutus?

I think definitely. I’m considering the following syntax:

type Datum {
  idx integer,
  x   integer
}

type Redeemer { // empty struct type acts like 'Unit'
}

// ScriptContext is builtin

// validator script succeeds if datum is correct
func main(datum Datum, redeemer Redeemer, ctx ScriptContext) bool {
  lst_a []integer = [1, 2, 3, 42]; // all variable declarations are typed
  lst_b []integer = 0:lst_a; // 'cons' operator. Names can never be shadowed
  x integer = datum.x; // conventional member access
  x == lst_b[datum.idx] // no 'return' keyword. Out of bounds indexing throws error
}

The nVidia / Khronos Group analogy is likely falling apart because I am a user space programmer :smiley:

Anywho, to your proposed syntax if we are bringing things like cons into the DSL does that mean we will also have built-ins like car, cdr, and friends to support lists and tuples? This wouldn’t be very JavaScript / TypeScript friendly …

I think for framing having a before and after comparison of an existing smart contract might help clarify the strengths and weaknesses. For example what would the Plutus playground vesting scheme look like in DSL?

Plutus Vesting contract example in the proposed DSL:

type VestingTranche {
    time   Time, // `amount` is available after this time
    amount Value 
}

type VestingParams {
    tranche1 VestingTranche,
    tranche2 VestingTranche,
    owner    PubKeyHash // the playground example uses PaymentPubKeyHash, but I couldn't find that type anywhere in the plutus-core ledger-api sources
}

func availableFrom(tranche VestingTranche, time Time) Value {
    if (time >= tranche.time) {
        tranche.amount    
    } else {
        zero()
    }
}

func remainingFrom(tranche VestingTranche, time Time) Value {
    tranche.amount - availableFrom(tranche, time)
}

// the compiler is smart enough to add an empty Datum and empty Redeemer as arguments to the actual main function
func main(ctx ScriptContext) bool {
    vestingParams VestingParams = {/*params interpolated from surrounding js*/}; 
    tx Tx = getTx(ctx);
    now Time = getTime(ctx); // start of txInfoValidRange
    remainingActual Value = valueLockedBy(tx, ownHash(ctx));
    remainingExpected Value = remainingFrom(vestingParams.tranche1, time) + remainingFrom(vestingParams.tranche2, time);
    remainingActual >= remainingExpected && txSignedBy(tx, vestingParams.owner)
}

In this example I am assuming the following builtins are available:

  • Time (also: Time >= Time etc.)
  • Value (also: Value + Value, Value - Value, Value >= Value etc.)
  • zero() Value (returns empty Value)
  • PubKeyHash
  • ScriptContext
  • Tx
  • getTx(ctx ScriptContext) Tx
  • getTime(ctx ScriptContext) Time
  • valueLockedBy(tx Tx, hash ValidatorHash) Value
  • ownHash(ctx ScriptContext) ValidatorHash
  • ValidatorHash (hash of validator script itself, not of validator node!)
  • txSignedBy(tx Tx, hash PubKeyHash) bool
4 Likes

Very interesting.

So Validator and the validate function would somehow be inferred by the underlying implementation and/or generated based on type models and schema?

Or would main here effectively be the validate function and so the DSL only covers on-chain portions of Plutus?

Would there be an option to pass a Validator to main to override the default behavior and or add additional logic via a function?

Redeemer in both cases is basically omitted for this use case but presumably that context could be passed into main as another argument?

Similarly Datum is also omitted and defaulted to be concise but could be passed to main as needed. In Plutus example this is because the vesting amounts are hard-coded but a more real use case would probably pass the amounts for each tranche. I didn’t see where the DSL assigns these values but we can assume it would be in main or some other off-chain code for parity.

I did not see where signing, constraints, and other such necessities were represented. Was this intentional as the goal is to generate raw transactions for submit via CLI, wallet, or other mechanism?

Would the transaction tree or list be a more appropriate output or what is the result after compiling the DSL?

For actual built-ins these would likely be wrapped by a DSL representation. Would it make sense to simply import these from a predefined DSL spec that mirrors Ledger and Plutus packages or would that require too much maintenance without some kind of MBSE to generate the JS wrappers to import?

I assume the bool return while omitted would effectively be for pass/fail of the compilation but in C-fashion an integer error code might allow more insight?

Apologies for woefully disorganized notes here. Pretty much just a stream of consciousness while reviewing.

Entrypoint
main is intended as the entrypoint of the validator script, so equivalent to the function often called validate in plutus. Datum, Redeemer and ScriptContext can each be optional arguments.

Entrypoint return value
I thought it was a good idea for the main function to return a Boolean instead of calling the non-pure error() function. Booleans are needed anyway, and this way we avoid introducing an additional concept to the user (i.e. the non-pure error() function).
Of course this entrypoint is just syntactic sugar for:

func final_main(datum Datum, redeemer Redeemer, ctx ScriptContext) {
  ifThenElse(main(datum, redeemer, ctx), (), error())
}

I wouldn’t return an interger error code. Instead it’s better to use the trace() function. But this is only relevant for playground–like debugging anyway. I don’t think there is any way to diagnose on-chain failures.

Off-chain stuff
This DSL wouldn’t be used for any off-chain stuff. Other tools will be needed for that (CLI, Blockfrost, etc.). API services could however br used through Javascript, and that kind of library could eventually be merged with the DSL library.

Builtins
I would make all builtins available in the global scope. I wouldn’t implement any import functionality. Import statements don’t make much sense for a DSL that doesn’t naturally reside in a file/directory structure.

Generation of Datum and Redeemer
Datum and Redeemer would be two struct-like types that need to be defined as part of the script (similar to how main must be defined). For actual transaction submission the DSL library can populate those data structures using JavaScript primitives/objects/lists, and generate the cbor bytearray needed for submission.

Compilation output
Compiling the script entrypoint would simply generate the bytearray representing the plutus-core logic.

1 Like

Excellent, that’s about what I was expecting. Thanks for the explanation and confirmations.

Presumably built-ins nested inline at global scope would prevent name resolution conflicts and other scope issues so long as internally they were mapped onto aliases for the Plutus representation. Users could simply look this up in the documentation. Simple is good.

Naming conventions of validate might be a minor syntax update that would coincide with redeem, datum, etc for better differentiation. Arguably main or an entry/end point could be inferred or injected by the compiler?

Would there be any mechanism to verify correctness such as generating a PAB stub or simulation? Obviously integration via the off-chain portion and validation would ultimately be use case specific and up to the developer to verify correctness of logic and result. It’s more a matter of any surrounding tooling and debug capabilities … you mention trace for example but would that be as good as it gets?

Related to usability do you have any thoughts about IDE integration with lexer and token parsing for syntax auto-completion or would that be left to plugin implementation per platform? I would be willing to create the JetBrains compatible open API plugin for Plutus DSL or whatever your working title is for this project for example … I’m sure someone out there would be willing to create a Visual Studio Code plugin as well.

The working title I’m using now is Plutus Light. Maybe you can come up with something better before I make the repository public.

It’s good a idea to name the entrypoint validate instead of main! I’m also thinking of changing the type keyword to data for user-defined struct-like types (type could be used for type-aliases instead).

I would disallow any name-shadowing, even with the 100+ builtins. This will improve readability.

There should indeed be a mechanism for verifying the correctness and debugging a script. The same DSL library could be used to do a pseudo evaluation of a script, or even go through it step-by-step. A simulation environment could set up the transactions so the ScriptContext has realistic content. We could host all these tools online as part of a plutus-light playground.

IDE integrations would probably be handled by individual IDE plugins. Because the DSL has a kind of standard C-like syntax, it shouldn’t be too hard to implement these.

Of course it’s important we get a working DSL compiler (and test it on some real-world cases) before we start working on the extras.

1 Like

Perhaps, something highlighting the standalone, independent, autonomous nature? IndiePlutus? AutoPlutus?

I really like your work! Hope, to come to test it out soon. Is the disassembler/decompiler that we talked about in another thread contained in it?

I kept Plutus Light as a name for now.

A good name that highlights the independent nature of the DSL probably shouldn’t include ‘Plutus’ at all. But I haven’t been able to think of anything yet.

The DSL compiler is now public: GitHub - OpenEngineer/plutus-light: Plutus-Light

It is basically a single, unminified, 6000-line ES6 javascript file that can easily be included in any nodejs or browser project. There are no dependencies. The library also contains the Plutus-Core disassembler mentioned earlier.

Warning: this is entirely untested. I’ve checked for correct serialization into Plutus-Core. I haven’t actually submitted any of the generated Plutus-Core to the testnet. Due to IOG’s incomplete documentation I’ve made a lot of assumptions concerning the data format of plutus-ledger-api opaque types, so many things can still fail.

Some syntax-related decisions I have made in the past week:

  • Primitive types are now CapitalCamelCase.
  • Function values aren’t entirely first class, and can’t be contained in lists or in any fields of a data type.
  • ByteArray instead of ByteString (for me ByteString sounds like ascii characters only, whereas it is in fact a list of uint8 integers).
  • The entrypoint of a script is still called main because the same scripts are used for minting, and calling the entrypoint validate would be confusing for minting scripts. Having different names depending on the usage (validating, minting, etc.) seemed like too much of a hassle.
  • No : operator (I will implement a prepend builtin function soon though)
  • Literal lists must be fully typed: so []Integer{0, 1, 2, 3} and not [0, 1, 2, 3]

Of course every single aspect of the DSL is still open for discussion.

Next stage is testing with cardano-cli.
I hope some people here are willing to help me with that. It would be nice to get an outside perspective at this point.

2 Likes

Good news: first working Plutus-Light script submitted on testnet today!

addr_test1wzlmzvrx48rnk9js2z6c0gnul2063hl2ptadw9cdzvvq7vgy4qmsu

The future is looking bright for all those who love Cardano, but hate Plutus/Haskell :stuck_out_tongue:

2 Likes

Noice! Adding this to my to-do list …

The first Plutus-Light script I tested above was a trivial AlwaysSucceeds script.

Today I successfully submitted a slightly more useful Time-Lock script (for details go to plutus-light/HOW_TO_CARDANO_CLI.md at main · OpenEngineer/plutus-light · GitHub):

addr_test1wz54prcptnaullpa3zkyc8ynfddc954m9qw5v3nj7mzf2wggs2uld

Turns out that the trace function is much more useful than I thought. cardano-cli basically does a complete and exact evaluation of the script before submitting. So the errors that cardano-cli catches will be exactly those encountered by the slot-leader, with the added advantage that you can debug the trace messages.

1 Like

Example of a working Subscription/Allowance contract:

addr_test1wpd49x0k6z3n5tdh4755txndjrmwhndt334flndheae3hes8a5jw4

Details can be found in the Plutus-Light repository. (I’ve moved the repository files around a bit, so the link in my previous post above no longer works).

Next we could to implement an English Auction contract. I should probably be the person to test it (my dev environment is currently quite streamlined). But perhaps someone else could try to write the contract? @DinoDude do you think you’re up for the task?

@Christian_Schmitz I meant to have a play last weekend but forgot about the holiday and other plans.

I was going to attempt converting on demand utility token minting over to DSL … should cover a lot of bases including datum, redeemer, multi-sig, etc.

PPP had an auction reference, actually this might be a better first try for user testing: plutus-pioneer-program/EnglishAuction.hs at 024ebd367bf6c4003b482bfb4c6db7d745ec85aa · input-output-hk/plutus-pioneer-program · GitHub

I’ll be your huckleberry :smiley:

I’ve already tested some basic minting: plutus-light/06-minting_policy_scripts.md at main · OpenEngineer/plutus-light · GitHub

Perhaps that can help you get started.

I like your DinoDapp concept a lot btw. It definitely is the perfect way to explore Cardano functionality (and the perfect way to test Plutus-Light :stuck_out_tongue: ).

An update:

Over the past month I’ve worked very hard to finalize Plutus-Light.

With some community input I’ve made the syntax more Rust-like. This basically means you can now define methods on structs and enums.

Most of the builtins have become methods as well.

The language has been renamed ‘Helios’ and the repository has been moved here. A user guide can be found here.

I’ve created a Helios playground, and I think the vesting-contract example on the Helios playground nicely demonstrates the syntax of this new language. I think this contract is much more readable than the original Plutus version.

1 Like