drawware: component-based software using diagrams as source code
[0D means "zero dependency" - a prerequisite for using diagrams to create software LEGO(R) blocks]
to create a new project:
git init
, then create a submodule with this repo in it
git submodule add https://github.com/guitarvydas/0D
cp 0D/templates/* .
mv dot-gitignore .gitignore
npm install ohm-js yargs
Prerequisites, you need to have installed:
Optional:
See Visual Syntax section, below.
https://github.com/guitarvydas/arith0d pdubroy's arith example built using this starter, emits code in 4 dialects: JS,Python,Wasm,CL
https://github.com/guitarvydas/abc0d simple language built using this sourdough starter
https://github.com/guitarvydas/llm0d - very simple usage of an LLM
https://github.com/guitarvydas/vsh0d Visual Shell (beginnings of)
https://github.com/guitarvydas/delay0d - handling probes and external code in 0D
https://github.com/guitarvydas/helloworld0d - trivial Hello World in 0D, in series and in parallel
(these are being worked on, many of these projects use earlier versions of 0D and were laid aside due to "life gets in the way" issues ... anyone welcome to join in)
https://github.com/guitarvydas/ason
https://github.com/guitarvydas/srw
https://github.com/guitarvydas/scm2js0d
based on grammar developed in https://guitarvydas.github.io/2020/12/09/OhmInSmallSteps.html
https://guitarvydas.github.io/2023/09/24/Find-and-Replace-SCN.html https://github.com/guitarvydas/find-and-replace
https://github.com/guitarvydas/ptpascal0d
I discussed my understanding of the Ceptre "Dungeon Crawler" example code in a presentation (Jan. 19, 2024, bumped from Jan. 17). I found that I needed to draw diagrams of my understanding of what the code was doing and sketched them out in the following slides. I am building a 0D drawware version of the diagrams, but, as yet, it is unfinished. Part of the process drove me down a rabbit hole, using 0D to "compile" (generate) Odin code (Leaf components) from some of the diagrams. The code generator ("compiler") is mostly working (gen0D), but needs a very few manual edits before being included in the dc0d project and appeasing the Odin type checker (things like the package name, quoted quotes in a few places, etc. - not worth spending more time automating, since my manual edits work fine, for now). As it stands, I use 14 simple layers of diagrams to reverse engineer the game.
jq
in several placesAn earlier version of the 0D repo contains a few more manual examples: https://github.com/guitarvydas/0dproto
This version uses the Odin language internally.
More languages are expected.
This section describes the "syntax" used in our compilable diagrams.
In general, the diagrams display a software program as a set of Components interconnected by Connection arrows.
There are two major classes of Component:
Containers are drawings that can contain Components and Connections. Containers can contain other Container components and/or Leaf components. In draw.io
, we represent each Container as a separate drawing on a separate tab in the draw.io
editor.
Leaves represent code. Leaves are represented on diagrams as dark blue rectangles. The actual code is in other files. In this demo, most of the Leaves are in leaf0d/leaf0d.odin
. That's only a convention, not a requirement. The code must provide two entry points
Typically, handle uses the send function to create output messages.
Components are templates (similar to classes). Components can be instantiated multiple times in a project. Each instantiation is unique and has a blob of unique storage (state) associated with it (similar to self in class-based languages).
Templates have unique names. Instances do not need to be explicitly named. The underlying 0D engine guarantees that each instance is unique (in a manner similar to references to objects in class-based languages).
Components have input and output ports. Input ports are drawn as white pills (small rounded rectangles), and output ports are drawn as dark-blue pills. Each port has a unique name. The names are scoped to be visible only within their parent component. The scope of input port names is unique from the scope of output port names within the same component, i.e. a component can use the same name for an input port as for an output port, but every input name must be different from every other input name within the same component and every output name must be different from every other output name within the same component. Different components can use the same names for ports as other components, for example, many components have input ports named "input". Those port names do not clash, they are "locally scoped" to their parent components.
Containers also have special ports called gates. Gates represent the top-level inputs and outputs of a Container. Input gates are drawn as white rhombuses with a name, and, output gates are drawn as blue rhombuses with a name.
Connections within Containers are used to hook output ports to input ports of child components in a 1:many and many:1 manner. And, Connections hook gates to ports.
Components can only send messages to their own output ports and gates. This is similar to input parameter lists in other programming languages, except that the parameter lists are for outputs (the set of input ports are like input parameter lists, the set of output ports are like output parameter lists). In other programming languages, a function cannot know where a particular input parameter came from. Likewise, a component cannot know where a particular output will go nor where an input came from.
Unlike procedures and functions in most programming languages, inputs to and outputs from a component can happen at any time, in any order. For example, if a component has two inputs A and B, it might be the case that inputs arrive on port B, and, some time later, inputs arrive on port A, or, the inputs might arrive very closely spaced together in time, or, multiple B inputs might arrive before any A inputs arrive, or, inputs never arrive on port A, and so on.
It turned out to be trivial to represent shell commands as Leaf Components. This repo includes a demo of such components. We call this VSH - for Visual SHell. It is possible to draw shell pipelines in draw.io
and to execute the pipelines. Combinations beyond simple pipelines are possible to express and are easier to express visually than as pure text. [Aside: an outcome of this approach is that it is convenient to visually express component programs that contain relatively heavy concepts. For example, a parser can be drawn and implemented as a single component with input ports and output ports. We are considering making A.I. and LLM components.]
First, I list summaries for the various sections, then the details of each section...
draw.io
version of /bin/bash, but simplified).![](doc/DPL%20syntax-handle().drawio.svg)
![](doc/DPL%20syntax-send().drawio.svg)
This version of 0D works in two main passes:
Step (1) uses the code in das2json/*.odin
, while step (2) uses the code in odin/*/*.odin
.
It is expected that we will be able to replace step (2) with programs written in different languages, like Python, JavaScript, Common Lisp, etc., but, we haven't done so yet. Ideally, we might rewrite step (2) in a meta language (currently called 'RT' - recursive text) that can then exhale code in Python, JavaScript, Common Lisp, etc., alleviating programmers from porting step (2) code into the various languages manually. We haven't done this yet.
Message is defined as a 3-tuple:
Tag is some sort of id. In this Odin implementation, the id is a string.
Data is a Datum.
Cause is a 2-tuple
ė
(spelled eh
in ASCII))ISU
means Isolated Software Unit. An ISU is a piece of code with 0 dependency leaks. It is like a procedure with a set of inputs and a set of outputs. It cannot call procedures in other ISUs, it can only send messages to other ISUs. Message sending is like IPCs in processes, not so-called Message Sending in Smalltalk (which is a just a form of subroutine calling with named parameters). If an ISU calls functions, those functions must be contained within the same ISU. If you were to draw a diagram of an ISU, it would be a rectangle (or other closed figure) with input ports and output ports (see the blue rectangles with ports in the above diagrams). The ports are not hard-wired to other parts of the system. An ISU is totally isolated and self-contained. See below for a discussion of parameters.
In 0D, we use the name Component to mean ISU.
Cause
allows programmers to track the provenance of Messages. Since each Message contains its cause, we can track back to the beginning of time, by following back-links.
A Datum is like a simplified object, with (at least) the following fields:
Contains drawings (i.e. source code), in .drawio format, for the demos
Tool that inhales .drawio drawings and exhales .json.
First, of 2, passes in the compilation process.
Foreseen to be used by compiler backends implemented in other languages, like Common Lisp, Python, etc. But, only the Odin back-end exists at this moment.
A small set of common definitions used by the front end (das2json) and the back end (odin, at present)
Implementation of 0D using the Odin language.
Odin is like a "better C". Programmers need to explicitly write code to manage memory.
Essentially, implementation of 0D in Odin is the most extreme use-case. Writing 0D in other, garbage-collected, languages should be as easy as simply stripping out the types from the Odin code and changing the syntax.
LLMs as Components.
Currently, contains only one example - "agency" with hard-wired command line parameters.
SVG Diagrams of the syntax.
All of the diagrams are contained in DPL syntax.drawio
in .drawio format, then exported to SVG.
If changes / updates are needed, one would edit DPL syntax.drawio
, then export to SVG.
Beginnings of 0D implementations in other languages.
WIP
0D in Common Lisp
0D written in a meta-syntax which I call Recursive Text.
The goal is to write 0D once - in RT - then use a transpiler technology, like OhmJS + RWR, to exhale 0D in various languages.
I'm still experimenting with what should be, and shouldn't be, in RT. Currently, I believe that there needs to be an explicit differentiation between operations and operands.
Operands are basically OOP and functions.
Operations are "syntax" (probably OhmJS will be used to make this quick and easy).
Pure functions in mathematics can be used to describe data, but don't do so well at describing control flow. Data and control flow are two separate issues (data is "shape", control flow is "meaning/stepping-through/interpretation of data"). Currently, in most exiting languages (e.g. Python, Rust, etc.) we use conditional evaluation of functions to fake out control flow. Yet, conditional evaluation and control flow are very separate issues, that should be treated separately.
Inspiration for this thread of thinking comes from gcc and Cordy's Orthogonal Code Generator work. GCC uses ideas from Fraser/Davidson peephole technology, namely RTL, to make it easier to generate great code. OCG is a further elaboration on such ideas and suggests the design and use of a declarative DSL for expressing code exhalation decision trees.
A group of parameters passed to a function in modern languages, is actually just a single, contiguous blob of data that is deconstructed into multiple data types. You can see this in assembler code. In assembler, we see that values are placed into a array, pointed to be the Stack Pointer. The parameter list is a heterogenous array (actually a CDR-less List) of various kinds of data. The receiving procedure immediately deconstructs the bytes in this array and acts like it has several typed data structures in the array.
Likewise, output values are just single, contiguous blobs of data that can be deconstructed into multiple types.
True multiple input parameters, are blobs of data that arrive at different times on different ports. True multiple output parameters, are blobs of data that are created at different times on different output ports.
A parameter list in conventional programming languages, is just a single input port. A complete blob of data arrives - all at once, with no time separation - at the input port. The receiving procedure / function immediately deconstructs the blob into distinct types of data. The input data, though, all arrives at once grouped together into a homogenous blob of data, hence, it is but a single input, regardless of how it is deconstructed.
Fundamentally, internal language doesn't matter when designing a solution to a problem.
If you need to optimize the solution (a big IF), then internal language and niggly details do matter. Note that optimization (e.g. type checking, etc.) reduces scalability, hence, should be used sparingly.
DPL means Diagrammatic Programming Languages.
The following DPL syntaxes could be used to describe the innards of ISUs (Isolated Software Units, i.e. Components). They simply need to be hooked to 0D technology to offer more flexible pluggability.
DaS means Diagrams as Syntax. DPL means Diagrammatic Programming Languages.
N.B. TPLs, such as Python, Rust, Javascript, etc. can, also, be used to write textual code for 0D Components. See demo_basics above.
components_to_include_in_project
components_to_include_in_project :: proc (leaves: ^[dynamic]zd.Leaf_Template) {
zd.append_leaf (leaves, zd.Leaf_Template { name = "trash", instantiate = trash_instantiate })
zd.append_leaf (leaves, std.string_constant ("rwr.ohm"))
}