{"id":15107579,"url":"https://github.com/alamvic/druid","last_synced_at":"2025-10-23T02:31:17.383Z","repository":{"id":65798500,"uuid":"598206546","full_name":"Alamvic/druid","owner":"Alamvic","description":"Meta-compiler to generate an optimised JIT compiler frontend based on an Interpreter definition","archived":false,"fork":false,"pushed_at":"2024-09-16T15:57:12.000Z","size":2682,"stargazers_count":8,"open_issues_count":59,"forks_count":6,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-09-25T21:25:30.679Z","etag":null,"topics":["meta-compiler","pharo","virtual-machine"],"latest_commit_sha":null,"homepage":"","language":"Smalltalk","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Alamvic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-06T16:16:35.000Z","updated_at":"2024-09-16T15:57:18.000Z","dependencies_parsed_at":"2023-11-15T18:27:36.947Z","dependency_job_id":"cae28b76-0a88-4e2b-b481-02c972bb9bdf","html_url":"https://github.com/Alamvic/druid","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alamvic%2Fdruid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alamvic%2Fdruid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alamvic%2Fdruid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alamvic%2Fdruid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Alamvic","download_url":"https://codeload.github.com/Alamvic/druid/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219871952,"owners_count":16554470,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["meta-compiler","pharo","virtual-machine"],"created_at":"2024-09-25T21:25:38.481Z","updated_at":"2025-10-23T02:31:17.376Z","avatar_url":"https://github.com/Alamvic.png","language":"Smalltalk","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Druid: Meta-interpretation for Just-In-Time compiler generation\n\nThe Druid project explores the automatic generation of machine code templates from bytecode interpreters using an abstract interpreter on the existing bytecode interpreter (a meta-interpreter).\nThis approach could benefit from having a single runtime implementation and having a JIT compiler generated from it.\n\n## Context\n\nJIT (Just-in-Time) compilers are an optimization technique often used for interpreted languages and virtual machines.\nThey allow to spend time optimizing only frequently used code, while falling back in slower execution engines for non-frequent code.\nFor example, the Pharo and the Java VM run on a bytecode interpreter and eventually compile machine code for methods that are frequently called.\n\nNowadays, the Pharo Virtual Machine is implemented in a subset of the Pharo language called Slang.\nThe Virtual Machine developers then benefit from the high-level tools used to work with Pharo code, such as the code editors, testing frameworks and debuggers.\nIn a later stage, the Virtual Machine code written in Slang is transpiled to C and then compiled to the target architectures.\n\nThe current Pharo JIT compiler that is part of the Virtual Machine, aka Cogit, implements an architecture based on templates of native code per bytecode.\nWhen a method is compiled, each bytecode is mapped to its corresponding template. All templates are concatenated to form a single machine code method.\nThis architecture has as drawback that the behavior of the Pharo language is duplicated in both the bytecode interpreter and their corresponding machine code templates.\n\nThe Druid project explores the automatic generation of machine code templates from bytecode interpreters using an abstract interpreter on the existing bytecode interpreter (a meta-interpreter).\n\n## Getting started\n\n### Installing Dependencies\n\n- You should also install the libraries unicorn and llvm.\n- If you are on linux, use your preferred package manager.\n- If you're on OSX, we recommend using homebrew.\n- In windows, we should check.\n\n#### Install Unicorn library for Pharo\n\n```bash\n# Unicorn 2 Setup for PharoVM\n# Instructions at: https://github.com/pharo-project/unicorn/blob/pharo-vm-unicorn2/docs/COMPILE.md\n\ngit clone https://github.com/pharo-project/unicorn.git\ncd unicorn\ngit checkout pharo-vm-unicorn2\nmkdir build; cd build\ncmake .. -DCMAKE_BUILD_TYPE=Release\nmake\n```\n\nNow replace possible already installed unicorn with the new compiled one, for example:\n```bash\nlocate libunicorn.2.dylib\nmv /opt/homebrew/Cellar/unicorn/2.0.1/lib/libunicorn.2.dylib /opt/homebrew/Cellar/unicorn/2.0.1/lib/libunicorn.2.old\ncp libunicorn.2.dylib /opt/homebrew/Cellar/unicorn/2.0.1/lib\n```\n\nOpen the Pharo image with the [Unicorn project loaded](https://github.com/pharo-project/pharo-unicorn) and check:\n```smalltalk\nUnicornLibrary uniqueInstance macModuleName \n```\n\n### Installing Druid\n\nDruid works for now on Pharo 11:\n\n```smalltalk\nEpMonitor disableDuring: [ \n\tMetacello new\n\t\trepository: 'github://Alamvic/druid:main';\n\t\tbaseline: 'Druid';\n\t\tload ]\n```\n\nThis will start the installation process, clone the dependencies, and load all necessary Pharo code.\n\n\n## Generate the JIT compiler\n\n### Generate the model\n\nTo generate the JIT compiler code from the interpreter:\n\n```st\nDRInterpreterToCompiler generateDruidJITModel\n```\n\nThat will generate the expected methods in the class `DruidJIT`, representing the JIT compiler of the VM.\n\nYou can check for friend methods of `generateDruidJITModel` to generate other models (with other options).\nFor example, `SimpleDruidJIT` is a version forcing a calling convention stack based.\n\n### Compile the Druid VM\n\nYou can generate the PharoVM as it is described on its wiki: https://github.com/pharo-project/pharo-vm/wiki/General-Build-Information.\n\nTo generate the VM using JIT compiler model generated by Druid, you have to use a different [Flavor]([url](https://github.com/pharo-project/pharo-vm/wiki/Flavors)):\n\n- _DruidVMFullCompilation_: Uses the `DruidJIT` model\n- _SimpleDruid_: Uses the `SimpleDruidJIT` model\n- _Check for more in the `PharoVMMaker`_\n\nAfter the process, you will have the an executable PharoVM. You can launch an image following the instructions: https://github.com/pharo-project/pharo-vm/wiki/PharoVM-Versions.\n\n## Prelude\n\n### (Prelude) An overview of the Pharo VM\n\nThe Pharo VM is a program that executes Pharo programs.\nIts main components are execution engine, and a memory manager.\nThe execution engine is made of an interpreter (a bytecode interpreter), and a JIT compiler that compiles to machine code methods that are used often.\nThe memory manager implements how objects are stored in memory, and an automatic reclamation of memory (usually called garbage collector).\n\nThe Pharo VM is implemented in a subset Pharo itself called Slang.\nThus, we can use Pharo tools to execute, test and debug the VM in a \"simulated\" manner.\nTo produce a productive VM the Slang Pharo code is transpiled to C and then compiled in the current architecture.\n\nDruid works on the Slang part of the VM, and its scope does not touch (for now) the C-code generation.\n\n### (Prelude) Bytecodes and a stack machine\n\nPharo methods are written as bytecodes and primitives.\nFor example, the following method is compiled as a method made of platform independent bytecode and an array of literals (also called literal frame) used in that method.\nThe bytecodes are virtual machine code instructions, the literals are the objects that represent fixed values used in that method.\nFor example, 1 and 17 are literals in this method. Besides numbers, other kind of literals are `'strings'`, literal arrays such as `#(a b c 1)` and characters `$A`. \n\n```smalltalk\nMyClass \u003e\u003e foo\n  ^ 1 + 17\n```\n\nWe can inspect the method above `MyClass \u003e\u003e foo` and see its bytecodes and literals.\nIn the list below the first number is the bytecode index, the second is the instruction bytes, and then there is a mnemonic of the instruction and arguments.\n\n```\n25 \u003c76\u003e pushConstant: 1\n26 \u003c20\u003e pushConstant: 17\n27 \u003cB0\u003e send: +\n28 \u003c7C\u003e returnTop\n```\n\nIt is important to understant that the Pharo bytecode is based on a stack machine (in contrast with register machines).\nThis means that most of the data between operations is exchanged through a stack.\nIn the example above, the first bytecode pushes a 1 into the stack and the second bytecode pushes a 2.\nThen, the third bytecode has to send a message `+`, so it pops two elements from the stack: one to use as the receiver, and one to use as an argument.\nWhen the execution of the message send finishes, the result is pushed to the stack.\nFinally, the last bytecode takes the top of the stack (the result of the addition), and returns it to the caller.\n\n### (Prelude) Understanding the bytecode interpreter\n\nWhen a bytecode method is executed by the interpreter, it iterates all bytecodes of a method and executes a VM routine for each of them.\nThe class implementing the bytecode interpreter is `StackInterpreter`.\nFor example, the `pushConstantOneBytecode` is the routine that pushes a 1 to the stack calling the `internalPush:` method.\nSince pushing the value 1 is a very common operation, a special bytecode is used for it to avoid putting the 1 in the literal frame.\n\n```smalltalk\nStackInterpreter \u003e\u003e pushConstantOneBytecode\n\n\tself fetchNextBytecode.\n\tself internalPush: ConstOne.\n```\n\nThe method `pushLiteralConstantBytecode` pushes a generic literal value to the stack also using `internalPush:`.\nThe value pushed is taken from the literal frame of the method, and the index is calculated from manipulating the `currentBytecode` variable.\nBytecode 33 pushes the first literal in the frame (33 bitAnd: 16r1F =\u003e 1), bytecode 34 pushes the second literal, and so on...\n\n```smalltalk\nStackInterpreter \u003e\u003e pushLiteralConstantBytecode\n\t\u003cexpandCases\u003e\n\tself\n\t\tcCode: \"this bytecode will be expanded so that refs to currentBytecode below will be constant\"\n\t\t\t[self fetchNextBytecode.\n\t\t\t self pushLiteralConstant: (currentBytecode bitAnd: 16r1F)]\n\t\tinSmalltalk: \"Interpreter version has fetchNextBytecode out of order\"\n\t\t\t[self pushLiteralConstant: (currentBytecode bitAnd: 16r1F).\n\t\t\t self fetchNextBytecode]\n```\n\nFinally, bytecodes such as the message send `+` are implemented as follows.\nFirst this bytecode gets the top 2 values from the stack.\nThen it checks if boths are integers, and if the result is an integer, in which case it pushes the value and finishes.\nIf they are not integers, it tries to add them as floats.\nIf that fails, it will perform a (slow) message send using the `normalSend` method.\n\n```smalltalk\nStackInterpreter \u003e\u003e bytecodePrimAdd\n\t| rcvr arg result |\n\trcvr := self internalStackValue: 1.\n\targ := self internalStackValue: 0.\n\t(objectMemory areIntegers: rcvr and: arg)\n\t\tifTrue: [result := (objectMemory integerValueOf: rcvr) + (objectMemory integerValueOf: arg).\n\t\t\t\t(objectMemory isIntegerValue: result) ifTrue:\n\t\t\t\t\t[self internalPop: 2 thenPush: (objectMemory integerObjectOf: result).\n\t\t\t\t\t^ self fetchNextBytecode \"success\"]]\n\t\tifFalse: [self initPrimCall.\n\t\t\t\tself externalizeIPandSP.\n\t\t\t\tself primitiveFloatAdd: rcvr toArg: arg.\n\t\t\t\tself internalizeIPandSP.\n\t\t\t\tself successful ifTrue: [^ self fetchNextBytecode \"success\"]].\n\n\tmessageSelector := self specialSelector: 0.\n\targumentCount := 1.\n\tself normalSend\n```\n\n### (Prelude) Understanding the existing Cogit JIT compiler\n\nWhen a bytecode method is executed a couple of times, the Pharo virtual machine decides to compile it to machine code.\nCompiling the method to machine code avoids performance overhead due to instruction fetching, and allows one to perform several optimizations.\nThe compilation of a machine code method goes pretty similar to the interpretation of a method.\nThe JIT compiler iterates the bytecode method and for each of the bytecodes it executes a code generation routine.\nThis means that we will (almost) have a counterpart for each of the VM methods implementing bytecode interpretation.\n\nFor example, the machine code generator implemented for `StackInterpreter\u003e\u003epushLiteralConstantBytecode` is `Cogit\u003e\u003egenPushLiteralConstantBytecode`.\n\n```smalltalk\nCogit \u003e\u003e genPushLiteralConstantBytecode\n\t^self genPushLiteralIndex: (byte0 bitAnd: 31)\n\nStackToRegisterMappingCogit \u003e\u003e genPushLiteralIndex: literalIndex \"\u003cSmallInteger\u003e\"\n\t\"Override to avoid the BytecodeSetHasDirectedSuperSend check, which is unnecessary\n\t here given the simulation stack.\"\n\t\u003cinline: false\u003e\n\t| literal |\n\tliteral := self getLiteral: literalIndex.\n\t^self genPushLiteral: literal\n```\n\nThe JIT'ted version of the addition bytecode (`genSpecialSelectorArithmetic`) is slightly more complicated, but it pretty much matches what it is done in the bytecode.\n\n### Overview of Druid\n\nIn Druid, a meta-interpreter analyzes the bytecode interpreter code and generates an intermediate representation from it.\nA compiler interface then generates machine code from the intermediate representation.\nThe output of the intermediate representation should have in general terms the same behaviour as the existing Cogit JIT compiler.\n\nTo verify the correctness of the compiler we use:\n - a machine code simulator (Unicorn)\n - a disassembler (llvm)\n\n(please see the links to these projects in the references)\n\n## The (meta-)interpreter\n\nThe setup is the following: we have one Pharo AST interpreter that we call the meta-interpreter that executes the code code of the \n`StackInterpreter` and generates the corresponding intermediate representation of `StackInterpreter` methods.\nCheck `Fun with interpreters` from the references to see more details on what an ASTs and abstract interpreters are.\nIn the code below, the meta-interpreter is called `DRASTInterpreter` (for the DruidAST interpreter) and it will analyse the methods \nof the stack interpreter returned byt the expression `Druid new newBytecodeInterpreter`.\nFor this task, the meta-interpreter uses an IR builder that is responsible for encapsulating the logic of IR building.\n\n```smalltalk\nbuilder := DRIRBuilder new.\nbuilder isa: #X64.\n\nastInterpreter := DRASTInterpreter new.\nastInterpreter vmInterpreter: Druid new newBytecodeInterpreter.\nastInterpreter irBuilder: builder.\n```\n\nThis AST interpreter then receives as input a list of bytecodes to analyze, it maps each bytecode to the routine to execute, \nobtains the AST and interprets each of the instructions of the AST using a visitor pattern.\n\n```smalltalk\nastInterpreter interpretBytecode: #[76].\n```\n\n### Visiting the AST\n\nThe `DRASTInterpreter` class implements a `visiting` protocol where the `visit` methods are grouped.\nMost of the visit methods are simple, like the following ones:\n\n```smalltalk\nDRASTInterpreter \u003e\u003e visitSelfNode: aRBSelfNode \n\n\t^ self receiver\n\nDRASTInterpreter \u003e\u003e visitTemporaryNode: aRBTemporaryNode \n\t\n\t^ currentContext temporaryNamed: aRBTemporaryNode name\n```\n\n### Context reification\n\nTo properly analyze the scope of temporary variables, the AST interpret reifies the contexts/stack frames.\nOn each method or block activation, a new context is pushed to the stack.\nThe temporary variables are then read and written from the current context.\nWhen a method or block returns, the current context is popped from the stack, to return to the caller context.\n\n### Interpreting Message sends\n\nThe most important operation in a Pharo program are message sends.\nMessage sends are used not only for normal method invocations, but also for common operators such as additions (`+`) and multiplications (`*`) and control flow such as conditionals (`ifTrue:`) and loops (`whileTrue:`).\nHowever, some of these special cases require special treatments when generating code.\nFor example, a multiplication should directly generate a normal multiplication and not require interpreting how that multiplication is implemented.\n\nTo manage such special cases, we keep a table in the interpreter that maps (special selector -\u003e special interpretation).\nWhen we find a message send in the AST, we lookup the selector in the table.\nIf we find a special case in the entry, we invoke that special entry in the interpreter.\nOtherwise, we lookup the method and activate the new method, which will recursively continue the interpretation\n\n```smalltalk\nDRASTInterpreter \u003e\u003e visitMessageNode: aRBMessageNode \n\t\n\t| arguments astToInterpret receiver |\n\t\n\t\"First interpret the arguments to generate instructions for them.\n\tIf this is a special selector, treat it specially with those arguments.\n\tOtherwise, lookup and interpret the called method propagating the arguments\"\n\treceiver := aRBMessageNode receiver acceptVisitor: self.\n\targuments := aRBMessageNode arguments collect: [ :e | e acceptVisitor: self ].\n\n\tspecialSelectorTable\n\t\tat: aRBMessageNode selector\n\t\tifPresent: [ :selfSelectorToInterpret |\n\t\t\t^ self perform: selfSelectorToInterpret with: aRBMessageNode with: receiver with: arguments ].\n\n\tastToInterpret := self\n\t\tlookupSelector: aRBMessageNode selector\n\t\treceiver: receiver\n\t\tisSuper: aRBMessageNode receiver isSuper.\n\t^ self interpretAST: astToInterpret withReceiver: receiver withArguments: arguments.\n```\n\nFor example, the following code snippet shows how the `internalPush:` is mapped as just a normal push instruction in the intermediate representation.\n\n```smalltalk\nDRASTInterpreter \u003e\u003e initialize\n\n\tsuper initialize.\n\tirBuilder := DRIRBuilder new.\n\t\n\tspecialSelectorTable := Dictionary new.\n    ...\n\tspecialSelectorTable at: #internalPush: put: #interpretInternalPushOn:receiver:arguments:.\n    \nDRASTInterpreter \u003e\u003e interpretInternalPushOn: aRBMessageNode receiver: aStackInterpreterSimulatorLSB arguments: aCollection \n\n    ^ irBuilder push: aCollection first\n```\n\n### The intermediate representation\n\nThe intermediate representation is generated by a builder object, instance of `DRIRBuilder`.\nThe AST interpreter collaborates with it to create instructions, new basic blocks and so on.\nThe intermediate representation that is created is somewhat inspired on a low-level intermediate representation as described in [Linear Scan Register Allocation for the Java HotSpot™ Client Compiler].\n\n### Some meta-interpretation of special cases\n\nNot all special cases generate instructions or interact with the DRIRBuilder.\nSome of them actually modify the state of the AST interpreter.\nFor example, a special case is the `fetchNextBytecode` instruction, that makes the interpreter move to the next byte in the list of bytecodes.\nTo simulate the same behaviour in our interpreter, its special case is implemented as follows:\n\n```smalltalk\nDRASTInterpreter \u003e\u003e interpretFetchNextBytecodeOn: aMessageSendNode receiver: aReceiver arguments: arguments\n\n\tself fetchNextInstruction\n    \nDRASTInterpreter \u003e\u003e fetchNextInstruction\n\n\tcurrentBytecode := instructionStream next\n```\n\n### The compiler interface\n\nOnce the interpreter finishes its job, the irBuilder will contain the intermediate representation instructions.\nWe can then generate machine code from them using the `DRIntermediateRepresentationToMachineCodeTranslator` class.\n\n```smalltalk\n\tastInterpreter irBuilder assignPhysicalRegisters.\n\n\tmcTranslator := DRIntermediateRepresentationToMachineCodeTranslator\n\t\ttranslate: builder instructions\n\t\twithCompiler: cogit.\n\n\taddress := cogit methodZone freeStart.\n\tendAddress := mcTranslator generate.\n```\n\n`DRIntermediateRepresentationToMachineCodeTranslator` iterates all the instructions and uses the double-dispatch pattern to \ngenerate code for each of them.\nIt uses a backend to generate the actual machine code.\n\n```smalltalk\naCollection do: [ :anIRInstruction | \n\tanIRInstruction accept: self ].\n\nDRIntermediateRepresentationToMachineCodeTranslator \u003e\u003e visitAdd:\nDRIntermediateRepresentationToMachineCodeTranslator \u003e\u003e visitLoad:\nDRIntermediateRepresentationToMachineCodeTranslator \u003e\u003e visitPush:\n```\n\n## About the Tests\n\nThe whole process has the following steps:\n\n1. The bytecode interpreter is interpreted to generate the instructions in the IRBuilder\n2. Then the physical registers will be allocated to the Druid instructions\n3. The DRInstructions generates the AbstractInstructions (Cogit) that basically are one to one representations with the machine code\n4. Compilation to machine code byte array\n\nSo, we have different tests to test different stages in the transformation:\n\n- The class `DRIRBuilderTest` tests the construction of the ir.\n- The class `DRASTInterpreterTest` tests the effect of interpreting ASTs\n- The DRIntermediateRepresentationToMachineCodeTranslatorTest simulates that each DR instrucion is correctly translated to the corresponding machine code\n- The class `DRSimulateGeneratedBytecodeTest` tests from end-to-end the generation of code of a bytecode and its execution in a machine code simulator\n\nOther test classes test a basic register allocation algorithm (`DRRegisterAllocationTest`), the generation of machine code from an IR without passing through the AST (`DRIntermediateRepresentationToMachineCodeTranslatorTest`) and the execution of machine code from an IR without passing through the AST  (`DRSimulateGeneratedCodeTest`).\n\n## Little exercises\n- In tbe book [https://github.com/SquareBracketAssociates/PatternsOfDesign/releases](https://github.com/SquareBracketAssociates/PatternsOfDesign/releases)\n- Chapter 4: Die and DieHandle double Dispatch (if you want to make sure that Double Dispatch has been understood do the Stone Paper Scissor Chapter)\n- Chapter 3 A little expression interpreter\n- Chapter 6 Understanding visitor \n- After reading [https://github.com/SquareBracketAssociates/Booklet-FunWithInterpreters](https://github.com/SquareBracketAssociates/Booklet-FunWithInterpreters)\n\n\n## References\n - Linear Scan Register Allocation for the Java HotSpot™ Client Compiler\n   http://www.ssw.uni-linz.ac.at/Research/Papers/Wimmer04Master/\n - Practical partial evaluation for high-performance dynamic language runtimes\n   https://dl.acm.org/doi/10.1145/3062341.3062381\n - Structure and Interpretation of Computer Programs\n   https://web.mit.edu/6.001/6.037/sicp.pdf\n - Fun with Interpreters\n   https://github.com/SquareBracketAssociates/Booklet-FunWithInterpreters/releases/download/continuous/fun-with-interpreters-wip.pdf\n - [Trace-Based Register Allocation](https://gitlab.inria.fr/RMOD/vm-papers/-/blob/master/compilation+JIT/2016_Trace-based%20Register%20Allocation%20in%20a%20JIT%20Compiler.pdf)\n - Paper explaining the motivations behind Sista Bytecode\n   https://github.com/SquareBracketAssociates/Booklet-PharoVirtualMachine/raw/master/bib/iwst2014_A%20bytecode%20set%20for%20adaptive%20optimizations.pdf\n\n - https://github.com/unicorn-engine/unicorn\n - https://github.com/guillep/pharo-unicorn\n - http://llvm.org/\n - https://github.com/guillep/pharo-llvmDisassembler\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falamvic%2Fdruid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falamvic%2Fdruid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falamvic%2Fdruid/lists"}