diff --git a/.gitignore b/.gitignore index 1407055..b2f817b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /target .pre-commit-config.yaml *.pdf +*.png diff --git a/docs/design/graphics.typ b/docs/design/graphics.typ index 320a46d..f14e65e 100644 --- a/docs/design/graphics.typ +++ b/docs/design/graphics.typ @@ -10,7 +10,8 @@ cetz.canvas({ // any preamble-ish stuff can go here set-style( - mark: (angle: 90deg) + mark: (angle: 90deg), + stroke: (cap: "round", join: "round"), ) what @@ -24,7 +25,7 @@ // smaller stuff -#let arrow(length: 1cm, lift: 4pt, stroke: 1pt) = graphic({ +#let arrow(length: 0.4cm, lift: 3pt, stroke: 1pt) = graphic({ line((0, lift), (rel: (length, 0)), mark: (end: ">", stroke: stroke)) // hack for the bounding box bottom @@ -34,7 +35,7 @@ // larger stuff -#let nodes( +#let sequence( distance: 3cm, arrow-spacing: 0.15cm, // cetz will support rounded rects in 0.2.0 @@ -64,14 +65,187 @@ }) #let stages-overview = canvas({ - nodes( + sequence( [Source], - [AST], [Graph IR], [Runtime], ) }) +// A few commands to help demonstration in the docs. +// Supply a string to mark the input or output as simple. +// (fwiw in typst, parenthesis around a single expression just evaluate the expression, and don't put it into an array) +#let cmds = ( + "const": ( + inputs: (), + outputs: ("data",), + ), + "open": ( + inputs: ("path",), + outputs: ("data",), + ), + "save": ( + inputs: ("data", "path"), + outputs: (), + ), + "show": ( + inputs: ("data",), + outputs: (), + ), + "invert": ( + inputs: ("base",), + outputs: ("",), + ), + "mask": ( + inputs: ("base", "stencil"), + outputs: ("masked", "rest"), + ), +) + +#let opposite(anchor) = { + ( + "bottom": "top", + "top": "bottom", + ) + .at(anchor) +} + +#let sockets( + start, + stop, + sockets, + socket-size: (0.5, 0.1), + socket-shape: "circle", + parent-name: "", + label-anchor: "bottom", +) = { + for (i, socket) in sockets.enumerate() { + let x-ratio = (i + 1) / (sockets.len() + 1) + let center = (start, x-ratio, stop) + + let socket-name = parent-name + "/" + socket + + let common-args = (name: socket-name, fill: black) + if socket-shape == "rect" { + rect( + (rel: ((0, 0), -0.5, socket-size), to: center), + (rel: socket-size), + ..common-args, + ) + } else if socket-shape == "circle" { + circle( + center, + radius: socket-size.at(1), + ..common-args, + ) + } else { + panic("unknown socket shape: `" + socket-shape + "`") + } + set-style(fill: none) + + // don't ask why, I don't know myself + let use-opposite-anchor = socket-shape == "circle" + content( + socket-name + "." + if use-opposite-anchor { opposite(label-anchor) } else { label-anchor }, + anchor: opposite(label-anchor), + box(inset: 0.25em, text(8pt, socket)), + ) + } +} + +#let node( + at, + size: (3, 1.5), + ty: none, + body: none, + name: "unnamed", +) = { + set-origin(at) + let label = [#ty] + if body != none { + label += [\ ] + text(0.7em, font: "IBM Plex Mono", body) + size.at(1) += 0.5 + } + rect((0, 0), (rel: size), name: name) + content(((0, 0), 0.5, size), align(center, label)) + + // input and output sockets + if ty == none { return } + let ty = cmds.at(ty) + + let sockets = sockets.with(parent-name: name) + sockets( + ((0, 0), "|-", size), + size, + ty.inputs, + ) + sockets( + (0, 0), + ((0, 0), "-|", size), + label-anchor: "top", + ty.outputs, + ) + + // helper text + let helper(base, label, where) = { + if not type(base) != list or base.len() != 0 { + content( + name + "." + where + "-left", + anchor: "right", + box(inset: 0.25em, text(fill: luma(75%), label)) + ) + } + } + + helper(ty.inputs, [in], "top") + helper(ty.outputs, [out], "bottom") + + // reset the origin transform so other nodes can still work in the global coord system + // can't use groups since otherwise the anchors are not exported + set-origin(((0, 0), -1, at)) +} + +#let connect(from, to, bend: 1.5, mark-cfg: (size: 0.25, offset: 0.1)) = { + bezier( + from, + to, + (rel: (0, -bend * 1cm), to: from), + (rel: (0, bend * 1cm), to: to), + ) + mark( + (rel: (0, mark-cfg.size + mark-cfg.offset), to: to), + (rel: (0, -mark-cfg.size)), + symbol: ">", + ) +} + +#let graph-example = canvas({ + let x = 3 + let y = -3 + node((-x, -0.75 * y), ty: "const", body: "\"base.png\"", name: "base") + node((x, -0.75 * y), ty: "const", body: "\"stencil.png\"", name: "stencil") + node((-x, 0), ty: "open", name: "a") + node((x, 0), ty: "open", name: "b") + node((0, y), ty: "mask", name: "c") + node((-x, 2 * y), ty: "invert", name: "d") + node((-x, 2.75 * y), ty: "show", name: "e") + node((x, 2.75 * y), ty: "show", name: "f") + + connect("base/data", "a/path") + connect("stencil/data", "b/path") + + connect("a/data", "c/base") + connect("b/data", "c/stencil") + + connect("c/masked", "d/base") + connect("d/", "e/data") + + connect("c/rest", "f/data", bend: 2.5) +}) + // literally just for standalone display of the graphics alone #import "../template.typ": conf #show: conf +#set page(width: auto, height: auto) + +#graph-example diff --git a/docs/design/iowo-design.typ b/docs/design/iowo-design.typ index ce8f5c5..e5f07f7 100644 --- a/docs/design/iowo-design.typ +++ b/docs/design/iowo-design.typ @@ -1,5 +1,6 @@ #import "../template.typ": conf #import "graphics.typ" +#import graphics: arrow #show: conf.with(title: [iOwO design], subtitle: [don't worry, we're just dreaming]) @@ -30,7 +31,7 @@ // at which point `query` doesn't find anything anymore #let terms = ( "source", - "ast", + "AST", "graph IR", "runtime", @@ -85,18 +86,29 @@ - algebraic enums - traits (`Numeric`...) +#pagebreak() + = Execution stages #graphics.stages-overview +iOwO operates in stages. This has a number of benefits and implications: + +- One can just work on one aspect of the stages without having to know about the rest. +- Bugs are easier to trace down to one stage. +- Stages are also replacable, pluggable and usable somewhere else. + - For example, one could write a Just-In-Time compiler to replace the runtime stage, while preserving the source #arrow() graph IR step. + +However, this also makes the architecture somewhat more complicated. So here we try our best to describe how each stage looks like. If you have any feedback, feel free to drop it on #link("https://forge.katzen.cafe/katzen-cafe/iowo/issues")[the issues in the repository]! + == Source ```iowo open base.png >| open stencil.png >| mask +|> show12 |> (invert | show) -|> show ``` Functional and pipeline-based. @@ -151,8 +163,8 @@ To handle each output of a streamer individually, they can be _split_: ```iowo mask -|> invert -- would invert the unmasked part |> show -- would show the masked part +|> invert -- would invert the unmasked part ``` ==== Combination @@ -193,16 +205,22 @@ Done with any of `--` or `//`. == Graph IR +#graphics.graph-example + The parsed representation of the source, and also what the runtime operates on. In a way, this is the AST, except that it's not a tree. +It is represented in iOwO using adjacencies, where essentially the vertices#footnote[Nodes or commands in this case.] and their edges#footnote[Connections or pipes in this case.] are stored separately. + === Optimizer Merges and simplifies commands in the graph IR. == Runtime +Runs through all commands in the graph IR. It does not have any significantly other representation, and despite its name there's _no_ bytecode involved. + === Scheduler Looks at the graph IR and decides when the VM should execute what. diff --git a/docs/template.typ b/docs/template.typ index fc7f8b2..899ab30 100644 --- a/docs/template.typ +++ b/docs/template.typ @@ -29,6 +29,7 @@ set text(..fonts.main) set heading(numbering: "A.1") + show link: text.with(fill: blue) show heading: it => text(..fonts.heading, it) show raw.where(block: true): box.with( fill: luma(95%),