Commits: 7

WIP: Setup TBB to evaluate the spec

Yesterday I've made an attempt to implement a Nushell interpreter for Tad Better Behavior. I got stuck on handling I/O. An interpreter needs to first write a line to indicate that it's ready to handle input, and then handle it line by line. After each line received on stdin it needs to respond with one or more lines on stdout and wait for more input. Once the input stream is closed, an interpreter should terminate.

I tried two approaches:

  1. Using input in a loop

This works very well, except the interpreter won't terminate when the input is closed, leaving zombie processes after each evaluation.

  1. Using lines without any explicit input

This is what I'm committing here. I've found this technique at https://github.com/nushell/nushell/issues/14901. It doesn't wait for input and terminates immediately, unless I remove (comment out) any output (to stdout and stderr) before calling the lines command. It seems like a bug in Nushell, so I've wrote an issue about it: https://github.com/nushell/nushell/issues/18033

index 7d5ec13..671416e 100644
--- a/flake.lock
+++ b/flake.lock
@@ -165,11 +165,11 @@
=        "rust-overlay": "rust-overlay"
=      },
=      "locked": {
-        "lastModified": 1775848233,
-        "narHash": "sha256-+V6K66AsFCxD0PmKOASSSFUdEjmAtIwX4XlQ+2JBrmk=",
+        "lastModified": 1776080969,
+        "narHash": "sha256-2uZxF6q0KOIH8tq3r65ZCz7dM+XYP3avtRP8IBq5zYY=",
=        "owner": "cachix",
=        "repo": "devenv",
-        "rev": "cf4f57c61f5dc9d58300bdf18102d9cf5b4f29ea",
+        "rev": "8d558a84fa38242a7f13781670fee1a6a8902b48",
=        "type": "github"
=      },
=      "original": {
@@ -565,11 +565,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1775657489,
-        "narHash": "sha256-v1KwZrIMGpteHPwxXvbapc7o3iduhU61phPUfyrnjM8=",
+        "lastModified": 1775984952,
+        "narHash": "sha256-FciKF0weMXVirN+ZBSniR4wpKx168cBa9IXhuaLOkkU=",
=        "owner": "cachix",
=        "repo": "nix",
-        "rev": "5c0da4397902105a84611c6d49e9d39a618ca025",
+        "rev": "e671135fc5b783798c444e4ece101f6b15ff0c46",
=        "type": "github"
=      },
=      "original": {
@@ -656,11 +656,11 @@
=    "nixpkgs-src": {
=      "flake": false,
=      "locked": {
-        "lastModified": 1773840656,
-        "narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=",
+        "lastModified": 1775888245,
+        "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=",
=        "owner": "NixOS",
=        "repo": "nixpkgs",
-        "rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512",
+        "rev": "13043924aaa7375ce482ebe2494338e058282925",
=        "type": "github"
=      },
=      "original": {
@@ -707,11 +707,11 @@
=        "nixpkgs-src": "nixpkgs-src"
=      },
=      "locked": {
-        "lastModified": 1774287239,
-        "narHash": "sha256-W3krsWcDwYuA3gPWsFA24YAXxOFUL6iIlT6IknAoNSE=",
+        "lastModified": 1776097194,
+        "narHash": "sha256-XD4DsgNcfXC5nlCxlAcCP5hSjTYlgLXEIoTj7fKkQg4=",
=        "owner": "cachix",
=        "repo": "devenv-nixpkgs",
-        "rev": "fa7125ea7f1ae5430010a6e071f68375a39bd24c",
+        "rev": "6e8a07b02f6f8557ffab71274feac9827bcc2532",
=        "type": "github"
=      },
=      "original": {
@@ -783,7 +783,8 @@
=      "inputs": {
=        "devenv": "devenv",
=        "nixpkgs": "nixpkgs_4",
-        "systems": "systems"
+        "systems": "systems",
+        "tad-better-behavior": "tad-better-behavior"
=      }
=    },
=    "rust-overlay": {
@@ -822,6 +823,45 @@
=        "type": "github"
=      }
=    },
+    "systems_2": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
+    "tad-better-behavior": {
+      "inputs": {
+        "devenv": [
+          "devenv"
+        ],
+        "nixpkgs": [
+          "nixpkgs"
+        ],
+        "systems": "systems_2"
+      },
+      "locked": {
+        "lastModified": 1775638737,
+        "narHash": "sha256-h2QcCWhV7hA+v8Tmyh5zmiZmeDLj23Rh5ciJwWiuhJI=",
+        "ref": "refs/heads/main",
+        "rev": "ed447ca6ee1a377bf4afcde6a785fdc0870e4f13",
+        "revCount": 162,
+        "type": "git",
+        "url": "http://codeberg.org/tad-lispy/tad-better-behavior"
+      },
+      "original": {
+        "type": "git",
+        "url": "http://codeberg.org/tad-lispy/tad-better-behavior"
+      }
+    },
=    "treefmt-nix": {
=      "inputs": {
=        "nixpkgs": [
index b6bf848..5b67689 100644
--- a/flake.nix
+++ b/flake.nix
@@ -4,6 +4,11 @@
=    systems.url = "github:nix-systems/default";
=    devenv.url = "github:cachix/devenv";
=    devenv.inputs.nixpkgs.follows = "nixpkgs";
+    tad-better-behavior = {
+      url = "git+http://codeberg.org/tad-lispy/tad-better-behavior";
+      inputs.nixpkgs.follows = "nixpkgs";
+      inputs.devenv.follows = "devenv";
+    };
=  };
=
=  nixConfig = {
@@ -11,7 +16,7 @@
=    extra-substituters = "https://devenv.cachix.org";
=  };
=
-  outputs = { self, nixpkgs, devenv, systems, ... } @ inputs:
+  outputs = { self, nixpkgs, devenv, systems, tad-better-behavior, ... } @ inputs:
=    let
=      forEachSystem = nixpkgs.lib.genAttrs (import systems);
=    in
@@ -20,6 +25,7 @@
=        (system:
=          let
=            pkgs = nixpkgs.legacyPackages.${system};
+            tbb = tad-better-behavior.packages.${system};
=          in
=          {
=            default = devenv.lib.mkShell {
@@ -27,7 +33,10 @@
=              modules = [
=                {
=                  # https://devenv.sh/reference/options/
-                  packages = [ pkgs.nushell ];
+                  packages = [
+                    pkgs.nushell
+                    tbb.default
+                  ];
=                }
=              ];
=            };
index 531b178..94eeb4c 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -1,3 +1,7 @@
+---
+interpreter: nu --stdin spec/interpreters/tbb.nu
+---
+
=# Basic
=
=This program generates a devlog for tad-lispy.com
new file mode 100644
index 0000000..2bc42df
--- /dev/null
+++ b/spec/interpreters/tbb.nu
@@ -0,0 +1,19 @@
+def main [] {
+    print --stderr "Interpreter listening..."
+
+    { type: InterpreterState, ready: true }
+    | to json --raw
+    | print
+
+    # FIXME: It doesn't wait for input if anything is printed
+    # See https://github.com/nushell/nushell/issues/18033
+    lines | each { str reverse | print --stderr }
+
+    # Alternative aproach to I/O. Also doesnt work:
+    # loop {
+    #     input | str reverse | print --stderr
+    # }
+
+    print --stderr "Interpreter done."
+}
+

Elaborate on projects list in a front matter

index 94eeb4c..9e97e08 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -39,7 +39,11 @@ So maybe I can just loop until I reach the time before `since`, emit the last fr
=
=## Generate the drafts
=
-For each day run `git log` in each project. If there is any output, dump it to `devlog/<date>/<project-name>.md`. So each file should contain a log from a single project that was developed on a given day. The project name should be listed in a front-matter (under `extra.projects`), so they can be linked to project page. Each commit should start with an `h2` followed by the message body and a complete diff.
+For each day run `git log` in each project. If there is any output, dump it to `devlog/<date>/<project-name>.md`. So each file should contain a log from a single project that was developed on a given day.
+
+The project name should be listed in a front-matter (under `extra.projects`), so they can be linked to projects pages. It's plural, and should be an array, because sometimes work in one repository is related to multiple projects - for example integrating TBB can be linked to the integration, but also TBB.
+
+Each commit should start with an `h2` followed by the message body and a complete diff.
=
=
=## Unlisted projects

Let it write project name in front matter

It writes it in two places:

The idea is, that title might be changed for editorial purposes (for example to reflect particular developments of the day) while projects list is more of a metadata (it can be used to link pages together). Also, projects are sometimes renamed. In that case, it's ok to change the metadata, but titles are often used to generate URLs, so it's better to keep them stable and not break any links.

I demonstrate how to change the cell path of projects in devlog-sample.toml. Nushell is really nice to work with data formats and structures.

index 698c93f..9a8d531 100644
--- a/devlog-sample.toml
+++ b/devlog-sample.toml
@@ -1,3 +1,10 @@
+[frontmatter]
+
+# By default projects are listed under extra.projects.  This example shows how
+# to use custom cell path to list them under taxonomies, for example to use with
+# Zola (https://www.getzola.org/documentation/content/taxonomies/).
+projects_cell_path = [ "taxonomies", "projects" ]
+
=[[projects]]
=name = "Tad Better Behavior"
=path = "~/Projects/tad-better-behavior"
@@ -7,5 +14,5 @@ name = "Better Tech Club website"
=path = "~/Projects/better-tech-club/bettertech.eu"
=
=[[projects]]
-name = "DevLog"
+name = "DevLog Extracavator"
=path = "."
index 7622b93..d27f57c 100755
--- a/devlog.nu
+++ b/devlog.nu
@@ -67,8 +67,16 @@ def "format diff" [] {
=
=def "format log" [
=    title: string
+    config: record
=] {
+    let frontmatter = { title: $title }
+    | insert $config.frontmatter.projects_cell_path [ $title ]
+
=    [
+        $"---"
+        ($frontmatter | to yaml)
+        $"---"
+        $""
=        $"# ($title)"
=        $""
=        $"Commits: ($in | length)"
@@ -84,8 +92,10 @@ def main [
=    --out-dir: path = "drafts"
=    --config-file: path = "devlog.toml"
=] {
+    # Read the config file, process and set some defaults
=    let config = open $config_file
=    | update projects { update path { path expand } } # Resolve all project paths
+    | upsert frontmatter.projects_cell_path { default { [ extra projects ] } | into cell-path } 
=
=    # TODO: Allow setting in a config file
=    mkdir $out_dir
@@ -120,7 +130,7 @@ def main [
=                }
=
=                $log
-                | format log $project.name
+                | format log $project.name $config
=                | save --force $out_path
=            } catch { |error|
=                print --stderr $"Project path: ($project_path)"

Fix the Nushell TBB interpreter I/O problem

Thanks to Juhan280 at GitHub:

https://github.com/nushell/nushell/issues/18033#issuecomment-4241910862

Apparently the first command takes over the input stream. It's still a bit fuzzy to me, but the solution with tee works, so I can continue from here. It's still a WIP, but at least the I/O seems to be working correctly.

index 2bc42df..5a8bfa2 100644
--- a/spec/interpreters/tbb.nu
+++ b/spec/interpreters/tbb.nu
@@ -1,18 +1,12 @@
=def main [] {
-    print --stderr "Interpreter listening..."
-
-    { type: InterpreterState, ready: true }
-    | to json --raw
-    | print
-
-    # FIXME: It doesn't wait for input if anything is printed
-    # See https://github.com/nushell/nushell/issues/18033
-    lines | each { str reverse | print --stderr }
-
-    # Alternative aproach to I/O. Also doesnt work:
-    # loop {
-    #     input | str reverse | print --stderr
-    # }
+    tee { # This preserves the input stream for the lines command
+        print --stderr "Interpreter listening..."
+        { type: InterpreterState, ready: true }
+        | to json --raw
+        | print
+    }
+    | lines
+    | each { str reverse | print --stderr }
=
=    print --stderr "Interpreter done."
=}

Setup the incoming handler in the TBB interpreter

I have to focus on something else now, but I want to set the stage for next steps.

index 5a8bfa2..4fefadb 100644
--- a/spec/interpreters/tbb.nu
+++ b/spec/interpreters/tbb.nu
@@ -6,8 +6,14 @@ def main [] {
=        | print
=    }
=    | lines
-    | each { str reverse | print --stderr }
+    | each { handle incoming }
=
=    print --stderr "Interpreter done."
=}
=
+def "handle incoming" [] {
+    $in
+    | from json
+    # TODO: Actually process TBB incoming messages and send responses
+    | print --stderr 
+}

Implement POC TBB interpreter in Nushell

All the basics are covered:

No observations yet. Also the interpreter and internals are all mixed together.

There is also some nonsense spec scenario to test it all. When evaluated it fails on purpose.

index 9e97e08..d20b3a7 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -90,3 +90,15 @@ By default `./devlog.toml` shall be used, but it can be changed with `--config-f
=A project can be flagged to be ignored with the `ignore` key.
=
=Any project that is not listed, but had activity within the period for which a devlog is being generated should produce a warning with useful instructions.
+
+## Interpreter test
+
+This is not real spec, just a quick test of the WIP interpreter.
+
+  * Do something `nice` and then `sleep` for `2` hours
+  
+    ``` text
+    Zzzzz.....
+    ```
+
+  * Do the twist `7` times
index 4fefadb..69652ea 100644
--- a/spec/interpreters/tbb.nu
+++ b/spec/interpreters/tbb.nu
@@ -1,3 +1,19 @@
+let steps = {
+    "Do something {0} and then {1} for {2} hours": { |how, what, how_long, data|
+        print --stderr $"OK boss. I will do something ($how) for ($how_long) hours and then I'll ($what)"
+        $data.code_blocks | first | get value | print --stderr 
+    },
+    "Do the twist {0} times": { |how_many, data|
+        $how_many | into int | let how_many
+        if $how_many > 5 {
+           error make "That's too many!"
+        }
+        for n in 1..$how_many {
+            $"Doing the twist ($n)" | print --stderr 
+        }
+    }
+}
+
=def main [] {
=    tee { # This preserves the input stream for the lines command
=        print --stderr "Interpreter listening..."
@@ -12,8 +28,27 @@ def main [] {
=}
=
=def "handle incoming" [] {
-    $in
-    | from json
-    # TODO: Actually process TBB incoming messages and send responses
-    | print --stderr 
+    let incoming = $in | from json
+
+    print --stderr --raw  $incoming
+    match $incoming.type {
+        Execute => {
+            try {
+                let step = $incoming.step
+                print --stderr $"Executing step ($step.description)"
+                let closure = $steps | get $step.variant
+                do $closure ...$step.arguments $step
+                { type: Success } | to json --raw | print
+            } catch { |err|
+                {
+                    type: Failure,
+                    reason: $err.msg
+                    hint: $err.rendered
+                } | to json --raw | print
+            }
+        },
+        _ => {
+            error make "Unsupported message type: ($incoming.type)" 
+        }
+    }
=}

Separate plumbing from the TBB interpreter

The "plumbing" involes setting up I/O, which IMO is a bit convoluted. Developers who implement interpreters shouldn't be bothered with it. They should just write steps' implementations and everything should work.

Now the is a tbb module holds the reusable parts and plumbing, while a new basic.nu interpreter defines the steps and calls tbb run, passing the steps. The tbb run (previously main) does all the I/O setup.

The only caveat is that tbb run must be the very first command in the interpreter. Otherwise the I/O is broken. See the (ongoing) discussion here:

https://github.com/nushell/nushell/issues/18033#issuecomment-4241910862

index d20b3a7..15b6d3f 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -1,5 +1,5 @@
=---
-interpreter: nu --stdin spec/interpreters/tbb.nu
+interpreter: nu --stdin spec/interpreters/basic.nu
=---
=
=# Basic
new file mode 100644
index 0000000..77cd3e2
--- /dev/null
+++ b/spec/interpreters/basic.nu
@@ -0,0 +1,19 @@
+use tbb.nu
+
+def main [] {
+    tbb run {
+        "Do something {0} and then {1} for {2} hours": { |how, what, how_long, data|
+            print --stderr $"OK boss. I will do something ($how) for ($how_long) hours and then I'll ($what)"
+            $data.code_blocks | first | get value | print --stderr 
+        },
+        "Do the twist {0} times": { |how_many, data|
+            $how_many | into int | let how_many
+            if $how_many > 5 {
+            error make "That's too many!"
+            }
+            for n in 1..$how_many {
+                $"Doing the twist ($n)" | print --stderr 
+            }
+        }
+    }
+}
index 69652ea..afb7882 100644
--- a/spec/interpreters/tbb.nu
+++ b/spec/interpreters/tbb.nu
@@ -1,20 +1,4 @@
-let steps = {
-    "Do something {0} and then {1} for {2} hours": { |how, what, how_long, data|
-        print --stderr $"OK boss. I will do something ($how) for ($how_long) hours and then I'll ($what)"
-        $data.code_blocks | first | get value | print --stderr 
-    },
-    "Do the twist {0} times": { |how_many, data|
-        $how_many | into int | let how_many
-        if $how_many > 5 {
-           error make "That's too many!"
-        }
-        for n in 1..$how_many {
-            $"Doing the twist ($n)" | print --stderr 
-        }
-    }
-}
-
-def main [] {
+export def run [steps: record]: string -> nothing {
=    tee { # This preserves the input stream for the lines command
=        print --stderr "Interpreter listening..."
=        { type: InterpreterState, ready: true }
@@ -22,12 +6,12 @@ def main [] {
=        | print
=    }
=    | lines
-    | each { handle incoming }
+    | each { handle incoming $steps }
=
=    print --stderr "Interpreter done."
=}
=
-def "handle incoming" [] {
+def "handle incoming" [steps: record] {
=    let incoming = $in | from json
=
=    print --stderr --raw  $incoming