diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 00000000..bf4b736e --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,13 @@ +;;; Directory Local Variables -*- no-byte-compile: t -*- +;;; For more information see (info "(emacs) Directory Variables") + +((nil . ((eval . (progn + (setq-local org-roam-directory + (file-truename + (file-name-concat + (locate-dominating-file default-directory ".dir-locals.el") + "docs/"))) + (setq-local org-roam-db-location + (file-name-concat org-roam-directory "org-roam.db")))) + (org-roam-capture-templates . (("d" "default" plain "%?" :target (file+head "${slug}.org" "#+title: ${title} +") :unnarrowed t)))))) diff --git a/.dockerignore b/.dockerignore index 63659823..843cf597 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,21 @@ -scratch-files/ -.git/ -.github/ -.githooks/ -.gitattributes -.gitmodules +# *.core +*.dot +*.fasl +*.log *.org -result +*.png +*.svg *~ -dockerfile .dockerignore +.git/ +.gitattributes +.githooks/ +.github/ .gitignore +.gitmodules +/.direnv/ +dockerfile +githooks/*.sample +public/ +result +scratch-files/ \ No newline at end of file diff --git a/.envrc b/.envrc index e9640925..50c6ef32 100644 --- a/.envrc +++ b/.envrc @@ -12,7 +12,7 @@ GUIX_PKGS="make python-codespell" if has guix; then - use guix --ad-hoc $PKGS_COMMON $GUIX_PKGS + use guix $PKGS_COMMON $GUIX_PKGS elif has nix-shell; then use nix -p $PKGS_COMMON $NIX_PKGS fi diff --git a/.gitignore b/.gitignore index 0fc5f101..9be62b11 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,10 @@ *.core *.svg result -docs/index.html +public/ + +# org-roam db +*.db githooks/*.sample /.direnv/ @@ -10,4 +13,5 @@ githooks/*.sample *.png *.log -*~ \ No newline at end of file +*~ +/docs/sitemap.org diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..7b1f60ca --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,65 @@ + +# Only run pipelines for merge requests, tags, and protected branches. +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_TAG + - if: $CI_COMMIT_REF_PROTECTED == "true" + +.base: + image: clfoundation/$LISP:latest + variables: + LISP: sbcl + QUICKLISP_ADD_TO_INIT_FILE: "true" + QUICKLISP_DIST_VERSION: "latest" + before_script: + - install-quicklisp + script: + - make test + rules: + - when: manual + +test: + extends: .base + variables: + LISP: sbcl + parallel: + matrix: + - STACK: + - sbcl + # TODO + # - abcl + # - ccl + # - ecl + + +## The job "doc" will re-run the tests, but I'm keeping that +## redundancy because the job "test" will be expanded to work on +## multiple cl implementations. + +# Build public/ folder using org-publish on docs/ +doc: + image: docker:24.0.7 + services: + - docker:24.0.5-dind + script: + - apk add --no-cache make + - make public + artifacts: + paths: + - public + rules: + - when: manual + +pages: + needs: + - job: doc + artifacts: true + script: + - echo "nothing to do!" + rules: + - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + - when: manual + artifacts: + paths: + - public diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..ef655468 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +###################################################################### +### Base layers, setup working directory and quicklisp +# FROM docker.io/clfoundation/${LISP}:${LISP_VERSION} as base +FROM alpine:3.18.4 as base + +RUN mkdir /breeze +WORKDIR /breeze + + +FROM base as quicklisp + +RUN apk add sbcl +COPY scripts/quicklisp.lisp scripts/quicklisp.lisp +RUN sbcl --non-interactive \ + --load scripts/quicklisp.lisp \ + --eval "(quicklisp-quickstart:install)" \ + --eval "(ql-util:without-prompting (ql:add-to-init-file))" + +###################################################################### +### Download all needed dependencies (for the main and the test +### systems). +FROM quicklisp as deps + +COPY breeze.asd . +COPY scripts/load-dependencies.lisp scripts/load-dependencies.lisp + +RUN sbcl --noinform --non-interactive \ + --load scripts/load-dependencies.lisp + + +FROM scratch as dependencies.core + +COPY --from=deps /breeze/dependencies.core /dependencies.core + +###################################################################### +### Run the tests and generate some documentation +FROM quicklisp as test + +COPY . . +RUN sbcl --core dependencies.core \ + --eval "(asdf:test-system '#:breeze)" + +FROM base as org-publish + +RUN apk add bash ca-certificates emacs + +COPY . . +COPY --from=test /breeze/docs /breeze/docs + +RUN emacs -Q --batch --load scripts/org-publish-project.el --kill +RUN ls +RUN ls /breeze/public + +FROM scratch as public + +COPY --from=org-publish /breeze/public / + + +###################################################################### +### This is where I left off + +# FROM deps as integration-tests +# RUN emacs -batch -l ert -l my-tests.el -f ert-run-tests-batch-and-exit diff --git a/README.md b/README.md index ba6c1c43..92628a6a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Breeze +# Breeze Breeze is a set of tools that aims to make lisp development a breeze (hence the name). @@ -6,157 +6,7 @@ Breeze is a set of tools that aims to make lisp development a breeze It is very much alpha quality, I'm experimenting with a lot of things in parallel. -Please take a look at the -[notes.org](https://github.com/fstamour/breeze/blob/master/notes.org) -to get a better idea of the things I wanna try. - -![ci](https://github.com/fstamour/breeze/actions/workflows/ci.yml/badge.svg) - -[github.com/fstamour/breeze](https://github.com/fstamour/breeze) - -## What is this? - -This is a git repository that contains lots of common lisp code that I -use to make developing with common lisp easier. It is a personal -project that I work on from time to time, but that I use (and break) -pretty much all the time. - -## Features - -* [Emacs integration](#emacs) -* Integration with quickproject -* Context-aware, configurable snippets and refactorings -* Command for quick code capture (trying out code in a new file) -* Implemented in common lisp to be able to port it to other editors in - the future - -Currently, breeze's main interface is emacs; `breeze.el` adds a few -commands and one minor-mode with one bindings (`C-.`). - -The one binding calls a command named `breeze-quickfix` (might rename -in the future). This command suggests applicable actions given the -current context (file name, file content, position in the file, -etc.). For example, if the file ends with ".asd" it will suggest a -command to insert a `defsystem` form. If breeze was already -configured, it will pre-fill the `:maintainer`, `:author` and -`:licence` fields. Another example is that if the file is empty, or -contains only comments, it will suggest to insert a `defpackage` or -`uiop:define-package` form. It is also able to detect when you're -trying to edit/evaluate forms that are in a package that doesn't -exists (_did you forget to evaluate the `defpackage` form_). - -The integration with quickproject is pretty simple and let's you -quickly create new projects from the comfort of your editor. The -integration consists of one command that asks you for some -information, like the project name and licence. It takes some default -values from breeze's configuration, but let's you change them. All -this to ease the use of quickproject. - -Another simple command that helps me is `breeze-capture`, it creates a -new file in a pre-determined (must be configured) folder and fills it -with some pre-configured content (template) and let's you code right -away. This could've easily be done in emacs (that's how I prototyped -the first version), but doing this in common lisp makes it easy to -port it to other editors (or just the repl) in the future. - -I must stress that this whole project is in constant flux, and until I -add more and more tests, stuff might break any time. - -## Goals and non-goals - -### Goals - -- Make it easier to develop in common lisp - - by any means -- With any editor (or even without one) -- Be as portable as possible -- Be useful to new and experimented developer (or even - non-developpers, we'll get there) - -### Non-goals - -- Replace slime, sly, slimv, slima, etc -- Replace existing test framework -- Force the user to use a set of conventions - - If there are conventions used by breeze it is only for convenience - and they should be customizable. - - e.g. Currently, some refactoring utilities only work when the user - use `cl:defpackage` (as opposed to `uiop:defpackage`) and there's - one `defpackage` per file, but it doesn't have to be that way. - -## Getting started - -1. Clone this repository in quicklisp's local-projects folder. - -2. Load the `breeze` system. From the repl: - - (ql:quickload "breeze") - -3. Load `/src/breeze.el` in emacs. - -And enable `breeze-mode` minor mode in `lisp-mode` automatically using -a hook: - - (add-hook 'lisp-mode-hook #'breeze-mode) - -Now you should be able to use `C-.` (control + period) in any lisp -buffer to bring up the "quickfix menu". - -### Configuring breeze - -> This is optional, but it will help make breeze more to your liking. - -Simply put this in your implementation's initialization file -(e.g. `~/.sbclrc` for sbcl). In the future, breeze might use it's own -configuration folder, but for now this will do. - - - (require '#:breeze.config) - - (setf breeze.config:*default-author* "your name" - breeze.config:*capture-folder* #p"~/capture/") - -### How to run the tests - -From the repl: - - (ql:quickload "breeze/test") - (asdf:test-system "breeze") - - -Or from the command line: - - ./scripts/test.sh - -### How to generate the documentation - -From the repl: - - (breeze.documentation::generate-documentation) - -Or from the command line: - - ./scripts/doc.sh - -With either method, the documentation is generated into to `docs/` -folder. - -### Contributing - -Start by forking and cloning this repository into quicklisp's -local-projects directory. - -Setup the pre-commit hook - - git config core.hooksPath githooks - -Look for TODOs in the code - - grep -ir --include='*.lisp' todo - # or - rg -i todo - -Peruse the [notes.org](notes.org). +More information in the [documentation](https://fstamour.gitlab.io/breeze/). ## Support me diff --git a/breeze.asd b/breeze.asd index 8acc41bc..eb5e38ee 100644 --- a/breeze.asd +++ b/breeze.asd @@ -1,3 +1,7 @@ +;;;; System definitions for breeze and auxiliary systems + + +;;; breeze.asd package (defpackage #:breeze.asd (:documentation "Package containing breeze's system defintions") @@ -5,16 +9,8 @@ (in-package #:breeze.asd) -(asdf:defsystem breeze/config - :description "Configurations for breeze." - :version "0" - :author "Francis St-Amour" - :licence "BSD 2-Clause License" - :pathname "src" - :serial t - :components - ((:file "configuration"))) - + +;;; breeze system (defsystem breeze :name "breeze" @@ -23,72 +19,70 @@ :author "Francis St-Amour" :licence "BSD 2-Clause License" :description "A system to help automate work." - :depends-on (breeze/config - ;; Multi-threading + :depends-on (;; Multi-threading bordeaux-threads chanl - trivial-timeout - ;; To create projects + ;; To create projects (scaffolds) quickproject ;; Utilities alexandria - anaphora - ;; cl-hash-util - cl-ppcre - closer-mop - str - uiop - ;; For documentation generation - 3bmd 3bmd-ext-code-blocks 3bmd-ext-tables spinneret - ;; For reading lisp - eclector - trivial-package-local-nicknames - ;; For some portability checks - trivial-features) + uiop) :pathname "src" :components ((:file "logging") (:file "cl") (:file "utils") - ;; TODO #++ - (:file "syntax-tree") - #++ - (:file "reader" :depends-on ("syntax-tree" "utils")) + (:file "string-utils" :depends-on ("utils")) + (:file "test-file" :depends-on ("utils" "string-utils")) + (:file "configuration") (:file "lossless-reader" :depends-on ("utils")) (:file "pattern") + (:file "egraph") + (:file "analysis" :depends-on ("lossless-reader" "pattern")) (:file "command" - :depends-on (#++"reader" - #++"syntax-tree" - "utils")) + :depends-on ("utils" + "configuration")) (:file "asdf") (:file "thread" :depends-on ("xref")) (:file "xref" :depends-on ("utils")) - (:file "documentation" :depends-on ("xref")) (:file "doctor") (:file "listener" :depends-on ("xref" "command")) - (:file "refactor" :depends-on (#++"reader" "command" "utils" "cl")) - (:file "project" :depends-on ("utils" "command")) - (:file "capture" :depends-on ("utils" "command"))) + (:file "suggestion" + :depends-on ("listener")) + (:file "refactor" :depends-on ("command" "utils" "cl")) + (:file "project" :depends-on ("utils" "command" "configuration")) + (:file "capture" :depends-on ("utils" "command" "configuration"))) :in-order-to ((test-op (load-op breeze/test))) :perform (test-op (o c) (uiop:symbol-call 'breeze.test.main 'run-breeze-tests))) -(defsystem breeze/docs + +;;; breeze/docs system + +(defsystem breeze/doc :description "Breeze component to generate documentation." :version "0.0.1" :author "Francis St-Amour" :licence "BSD 2-Clause License" - :depends-on (breeze) + :depends-on (breeze + ;; For documentation generation + spinneret + closer-mop + cl-ppcre) :pathname "src" - :serial t + :serial nil ; <- :components - ((:file "documentation"))) + ((:file "documentation") + (:file "report"))) -(defsystem "breeze/kite" + +;;; breeze/kite system + +(defsystem breeze/kite :description "A breeze in a parachute makes a kite: utils for parachute" :version "0.0.1" :author "Francis St-Amour" @@ -99,21 +93,25 @@ :components ((:file "kite"))) -(defsystem "breeze/test" + +;;; breeze/test system + +(defsystem breeze/test :description "Tests for the breeze system." :version "0" :author "Francis St-Amour" :licence "BSD 2-Clause License" - :depends-on (breeze parachute breeze/kite) + :depends-on (breeze parachute breeze/kite breeze/doc) :pathname "tests" :serial t :components ((:file "utils") (:file "logging") - #++ - (:file "reader") + (:file "lossless-reader.randmized") (:file "lossless-reader") (:file "pattern") + (:file "analysis") + (:file "egraph") (:file "command") (:file "refactor") (:file "dummy-package") diff --git a/compilation-error-regexp.txt b/compilation-error-regexp.txt new file mode 100644 index 00000000..c9145235 --- /dev/null +++ b/compilation-error-regexp.txt @@ -0,0 +1,993 @@ +# Here's a few example of errors that could be detected + + + +ASDF could not load myelin because +Component ASDF/USER::FIND-PORT not found, required by #. +While evaluating the form starting at line 2, column 0 + of #P"/opt/myelin/myelin.test.asd": +While evaluating the form starting at line 7, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a ASDF/FIND-COMPONENT:MISSING-DEPENDENCY in thread +#: + Component ASDF/USER::FIND-PORT not found, required by # + + + + + + +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-test". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file. +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-dev". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file. +ASDF could not load myelin because +Component ASDF/USER::COM.INUOE.JZON not found, required by #. +While evaluating the form starting at line 2, column 0 + of #P"/opt/myelin/myelin.test.asd": +While evaluating the form starting at line 7, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a ASDF/FIND-COMPONENT:MISSING-DEPENDENCY in thread +#: + Component ASDF/USER::COM.INUOE.JZON not found, required by # + + +(ASDF/FIND-COMPONENT:RESOLVE-DEPENDENCY-NAME # ASDF/USER::COM.INUOE.JZON NIL) + error finding frame source: Bogus form-number: the source file has probably + changed too much to cope with. + source: NIL + + + + +-*- mode: compilation; default-directory: "~/dev/myelin/" -*- +Compilation started at Fri Jan 26 23:17:27 + +make -k +docker build -f myelin.dockerfile -t myelin . +Sending build context to Docker daemon 257.5kB +Step 1/16 : FROM docker.io/ubuntu:20.04 + ---> f78909c2b360 +Step 2/16 : RUN apt-get update && apt-get upgrade -y && apt-get install -y sbcl curl vim emacs graphviz + ---> Using cache + ---> d62c83b6557e +Step 3/16 : RUN cd /tmp && curl -O https://beta.quicklisp.org/quicklisp.lisp && sbcl --noinform --disable-ldb --lose-on-corruption --load quicklisp.lisp --eval '(quicklisp-quickstart:install :path "/opt/quicklisp")' --eval '(ql::without-prompting (ql:add-to-init-file))' && cp $HOME/.sbclrc /etc/sbclrc && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + ---> Using cache + ---> 942ecf286883 +Step 4/16 : RUN mkdir /opt/myelin + ---> Using cache + ---> 800051f9dda9 +Step 5/16 : WORKDIR /opt/myelin + ---> Using cache + ---> de7135cb0a2d +Step 6/16 : COPY myelin-dependencies.asd /opt/myelin/ + ---> 164dcd2c9497 +Step 7/16 : RUN sbcl --noinform --disable-ldb --lose-on-corruption --eval '(require :asdf)' --eval '(load "myelin-dependencies.asd")' --eval "(ql:quickload '(#:myelin-dependencies #:swank))" + ---> Running in 2a0aec266b32 +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "hunchentoot": + Load 2 ASDF systems: + asdf uiop + Install 20 Quicklisp releases: + alexandria babel bordeaux-threads cffi chunga cl+ssl + cl-base64 cl-fad cl-ppcre flexi-streams global-vars + hunchentoot md5 rfc2388 split-sequence + trivial-backtrace trivial-features trivial-garbage + trivial-gray-streams usocket +; Fetching # +; 9.15KB +================================================== +9,368 bytes in 0.00 seconds (9148.44KB/sec) +; Fetching # +; 12.23KB +================================================== +12,522 bytes in 0.00 seconds (12228.52KB/sec) +; Fetching # +; 16.33KB +================================================== +16,719 bytes in 0.00 seconds (8163.57KB/sec) +; Fetching # +; 153.74KB +================================================== +157,428 bytes in 0.06 seconds (2562.30KB/sec) +; Fetching # +; 24.69KB +================================================== +25,285 bytes in 0.00 seconds (12346.19KB/sec) +; Fetching # +; 10.13KB +================================================== +10,373 bytes in 0.00 seconds (10129.88KB/sec) +; Fetching # +; 11.43KB +================================================== +11,705 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 91.60KB +================================================== +93,803 bytes in 0.05 seconds (1991.40KB/sec) +; Fetching # +; 435.47KB +================================================== +445,922 bytes in 0.10 seconds (4536.15KB/sec) +; Fetching # +; 267.55KB +================================================== +273,967 bytes in 0.06 seconds (4116.09KB/sec) +; Fetching # +; 257.22KB +================================================== +263,396 bytes in 0.07 seconds (3572.54KB/sec) +; Fetching # +; 90.20KB +================================================== +92,364 bytes in 0.05 seconds (1768.61KB/sec) +; Fetching # +; 7.87KB +================================================== +8,055 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 20.44KB +================================================== +20,929 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 10.75KB +================================================== +11,005 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 11.10KB +================================================== +11,368 bytes in 0.00 seconds (11101.56KB/sec) +; Fetching # +; 3.50KB +================================================== +3,581 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 2991.12KB +================================================== +3,062,902 bytes in 0.45 seconds (6632.18KB/sec) +; Fetching # +; 55.87KB +================================================== +57,207 bytes in 0.02 seconds (2539.37KB/sec) +; Fetching # +; 262.75KB +================================================== +269,060 bytes in 0.09 seconds (2887.41KB/sec) +; Loading "hunchentoot" +.................................................. +[package impl-specific-gray]...................... +[package trivial-gray-streams].................... +[package chunga].................................. +[package cl-base64]............................... +[package alexandria].............................. +[package alexandria-2]............................ +[package global-vars]............................. +[package trivial-garbage]......................... +[package bordeaux-threads]........................ +[package bordeaux-threads-2]...................... +[package cl-fad].................................. +[package path].................................... +[package cl-ppcre]................................ +.................................................. +[package flexi-streams]........................... +.................................................. +.................................................. +.................................................. +[package babel-encodings]......................... +[package babel]................................... +.................................................. +[package cffi-sys]................................ +[package cffi-features]........................... +[package cffi].................................... +.................................................. +[package cl+ssl/config]........................... +[package split-sequence].......................... +[package usocket]................................. +[package cl+ssl].................................. +[package md5]..................................... +[package rfc2388]................................. +[package trivial-backtrace]....................... +[package url-rewrite]............................. +[package hunchentoot]........................... +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "spinneret": + Load 9 ASDF systems: + alexandria asdf bordeaux-threads cl-ppcre global-vars + split-sequence trivial-garbage trivial-gray-streams uiop + Install 18 Quicklisp releases: + anaphora closer-mop introspect-environment iterate + lisp-namespace mgl-pax named-readtables parenscript + parse-declarations parse-number serapeum spinneret + string-case trivia trivial-cltl2 trivial-file-size + trivial-macroexpand-all type-i +; Fetching # +; 1.92KB +================================================== +1,968 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 3.15KB +================================================== +3,225 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 5.94KB +================================================== +6,081 bytes in 0.00 seconds (5938.48KB/sec) +; Fetching # +; 6.24KB +================================================== +6,394 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 9.56KB +================================================== +9,786 bytes in 0.00 seconds (9556.64KB/sec) +; Fetching # +; 23.25KB +================================================== +23,805 bytes in 0.00 seconds (23247.07KB/sec) +; Fetching # +; 338.01KB +================================================== +346,126 bytes in 0.12 seconds (2725.92KB/sec) +; Fetching # +; 60.61KB +================================================== +62,061 bytes in 0.02 seconds (3030.32KB/sec) +; Fetching # +; 8.87KB +================================================== +9,081 bytes in 0.00 seconds (8868.16KB/sec) +; Fetching # +; 5.58KB +================================================== +5,715 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 35.80KB +================================================== +36,664 bytes in 0.01 seconds (2557.48KB/sec) +; Fetching # +; 9.47KB +================================================== +9,695 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 246.36KB +================================================== +252,271 bytes in 0.07 seconds (3329.17KB/sec) +; Fetching # +; 958.83KB +================================================== +981,840 bytes in 0.20 seconds (4677.21KB/sec) +; Fetching # +; 26.11KB +================================================== +26,739 bytes in 0.00 seconds (13056.15KB/sec) +; Fetching # +; 6.16KB +================================================== +6,303 bytes in 0.00 seconds (6155.27KB/sec) +; Fetching # +; 101.52KB +================================================== +103,952 bytes in 0.04 seconds (2360.83KB/sec) +; Fetching # +; 30.69KB +================================================== +31,424 bytes in 0.01 seconds (2789.77KB/sec) +; Loading "spinneret" +[package mgl-pax.asdf]............................ +[package anaphora]................................ +[package anaphora-basic].......................... +[package anaphora-symbol]......................... +[package dref-ext]................................ +[package dref].................................... +[package dref-ext]................................ +[package dref].................................... +[package mgl-pax]................................. +[package mgl-pax]................................. +[package editor-hints.named-readtables]........... +[package editor-hints.named-readtables]........... +[package parenscript]............................. +[package ps-js-symbols]........................... +[package ps-dom1-symbols]......................... +[package ps-dom2-symbols]......................... +[package ps-window-wd-symbols].................... +[package ps-dom-nonstandard-symbols].............. +[package ps-dhtml-symbols]........................ +[package ps-js]................................... +.................................................. +[package trivia.level0]........................... +[package trivia.level1]........................... +[package trivia.fail]............................. +[package trivia.skip]............................. +[package trivia.next]............................. +[package trivia.level1.impl]...................... +[package lisp-namespace].......................... +[package closer-mop].............................. +[package closer-common-lisp]...................... +[package closer-common-lisp-user]................. +[package trivial-cltl2]........................... +[package trivia.level2]........................... +[package trivia.level2.impl]...................... +.................................................. +[package introspect-environment].................. +[package type-i].................................. +[package iterate]................................. +[package trivia.balland2006]...................... +[package string-case]............................. +[package org.mapcar.parse-number]................. +[package tcr.parse-declarations-1.0].............. +[package trivial-file-size]....................... +[package trivial-macroexpand-all]................. +[package serapeum.sum]............................ +[package serapeum]................................ +[package serapeum-user]........................... +[package serapeum.unlocked]....................... +[package serapeum/op]............................. +.................................................. +[package serapeum/static-let]..................... +.................................................. +.................................................. +.................................................. +.................................................. +[package serapeum/vector=]........................ +[package serapeum/mop]............................ +[package serapeum/internal-definitions]........... +.................................................. +[package serapeum/dispatch-case].................. +[package serapeum/generalized-arrays]............. +[package serapeum/contrib/hooks].................. +[package spinneret]............................... +[package spinneret-user].......................... +[package spinneret.tag].......... +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "cl-who": + Load 1 ASDF system: + asdf + Install 1 Quicklisp release: + cl-who +; Fetching # +; 24.24KB +================================================== +24,823 bytes in 0.00 seconds (4848.24KB/sec) +; Loading "cl-who" +[package cl-who].... +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "str": + Load 3 ASDF systems: + asdf cl-ppcre cl-ppcre-unicode + Install 2 Quicklisp releases: + cl-change-case cl-str +; Fetching # +; 5.04KB +================================================== +5,163 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 24.63KB +================================================== +25,222 bytes in 0.00 seconds (24630.86KB/sec) +; Loading "str" +To load "cl-unicode": + Load 2 ASDF systems: + asdf cl-ppcre + Install 1 Quicklisp release: + cl-unicode +; Fetching # +; 1260.39KB +================================================== +1,290,638 bytes in 0.26 seconds (4810.64KB/sec) +; Loading "cl-unicode" +[package cl-unicode].............................. +[package cl-unicode-names]...................... +To load "str": + Load 1 ASDF system: + str +; Loading "str" +[package cl-ppcre-unicode]........................ +[package cl-change-case].......................... +[package str]...... +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "cl-strings": + Load 1 ASDF system: + asdf + Install 1 Quicklisp release: + cl-strings +; Fetching # +; 10.94KB +================================================== +11,200 bytes in 0.00 seconds (10937.50KB/sec) +; Loading "cl-strings" +[package cl-strings]... +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "cl-hash-util": + Load 1 ASDF system: + asdf + Install 1 Quicklisp release: + cl-hash-util +; Fetching # +; 7.08KB +================================================== +7,252 bytes in 0.00 seconds (0.00KB/sec) +; Loading "cl-hash-util" +[package cl-hash-util].. +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "cl-cron": + Load 2 ASDF systems: + asdf bordeaux-threads + Install 1 Quicklisp release: + cl-cron +; Fetching # +; 15.99KB +================================================== +16,376 bytes in 0.00 seconds (7996.09KB/sec) +; Loading "cl-cron" +[package cl-cron].. +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "vas-string-metrics": + Load 1 ASDF system: + asdf + Install 1 Quicklisp release: + vas-string-metrics +; Fetching # +; 6.73KB +================================================== +6,888 bytes in 0.00 seconds (0.00KB/sec) +; Loading "vas-string-metrics" +[package vas-string-metrics] +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "apply-argv": + Load 1 ASDF system: + alexandria + Install 1 Quicklisp release: + apply-argv +; Fetching # +; 2.39KB +================================================== +2,451 bytes in 0.00 seconds (2393.55KB/sec) +; Loading "apply-argv" +[package apply-argv]. +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "drakma": + Load 7 ASDF systems: + asdf chunga cl+ssl cl-base64 cl-ppcre flexi-streams + usocket + Install 3 Quicklisp releases: + chipz drakma puri +; Fetching # +; 28.49KB +================================================== +29,178 bytes in 0.00 seconds (7123.54KB/sec) +; Fetching # +; 36.67KB +================================================== +37,547 bytes in 0.01 seconds (3055.58KB/sec) +; Fetching # +; 73.55KB +================================================== +75,317 bytes in 0.04 seconds (2101.48KB/sec) +; Loading "drakma" +[package puri].................................... +[package chipz]................................... +[package drakma]........ +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "lquery": + Load 1 ASDF system: + asdf + Install 7 Quicklisp releases: + array-utils clss documentation-utils form-fiddle lquery + plump trivial-indent +; Fetching # +; 5.46KB +================================================== +5,590 bytes in 0.00 seconds (5458.98KB/sec) +; Fetching # +; 3.46KB +================================================== +3,538 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 8.68KB +================================================== +8,889 bytes in 0.00 seconds (8680.66KB/sec) +; Fetching # +; 50.05KB +================================================== +51,256 bytes in 0.03 seconds (2002.19KB/sec) +; Fetching # +; 20.87KB +================================================== +21,372 bytes in 0.00 seconds (6957.03KB/sec) +; Fetching # +; 6.76KB +================================================== +6,926 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 37.71KB +================================================== +38,610 bytes in 0.01 seconds (2693.22KB/sec) +; Loading "lquery" +[package array-utils]............................. +[package trivial-indent].......................... +[package documentation-utils]..................... +[package form-fiddle]............................. +[package plump-lexer]............................. +[package plump-dom]............................... +[package plump-parser]............................ +[package plump]................................... +[package clss].................................... +[package lquery].................................. +[package lquery-funcs]............................ +[package lquery-macros]........... +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "clingon": + Load 4 ASDF systems: + alexandria asdf split-sequence uiop + Install 4 Quicklisp releases: + bobbin cl-reexport clingon with-user-abort +; Fetching # +; 1.07KB +================================================== +1,094 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 2.48KB +================================================== +2,538 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 6.18KB +================================================== +6,329 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 597.65KB +================================================== +611,998 bytes in 0.13 seconds (4527.68KB/sec) +; Loading "clingon" +[package bobbin].................................. +[package cl-reexport]............................. +[package with-user-abort]......................... +[package clingon.utils]........................... +[package clingon.conditions]...................... +[package clingon.options]......................... +[package clingon.command]......................... +[package clingon] +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "find-port": + Load 2 ASDF systems: + asdf usocket + Install 1 Quicklisp release: + find-port +; Fetching # +; 2.47KB +================================================== +2,534 bytes in 0.00 seconds (0.00KB/sec) +; Loading "find-port" +[package find-port] +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" +To load "com.inuoe.jzon": + Load 7 ASDF systems: + asdf closer-mop documentation-utils flexi-streams + trivial-features trivial-gray-streams uiop + Install 2 Quicklisp releases: + float-features jzon +; Fetching # +; 11.27KB +================================================== +11,540 bytes in 0.00 seconds (0.00KB/sec) +; Fetching # +; 65.71KB +================================================== +67,292 bytes in 0.04 seconds (1685.00KB/sec) +; Loading "com.inuoe.jzon" +[package float-features].......................... +[package com.inuoe.jzon/eisel-lemire]............. +[package com.inuoe.jzon/ratio-to-double].......... +[package com.inuoe.jzon/schubfach]................ +[package com.inuoe.jzon].......................... +...... +To load "myelin-dependencies": + Load 1 ASDF system: + myelin-dependencies +; Loading "myelin-dependencies" + +To load "swank": + Load 1 ASDF system: + asdf + Install 1 Quicklisp release: + slime +; Fetching # +; 807.68KB +================================================== +827,061 bytes in 0.18 seconds (4512.16KB/sec) +; Loading "swank" +[package swank-loader]............................ +[package swank/backend]........................... +[package swank/rpc]............................... +[package swank/match]............................. +[package swank-mop]............................... +[package swank]................................... +[package swank/source-path-parser]................ +[package swank/source-file-cache]................. +[package swank/sbcl].............................. +[package swank/gray].............................. +........... +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-util.lisp" (written 27 JAN 2024 04:18:09 AM): +. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-util.fasl +; compilation finished in 0:00:00.012 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-repl.lisp" (written 27 JAN 2024 04:18:09 AM): +...................................... +[package swank-repl].. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-repl.fasl +; compilation finished in 0:00:00.038 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-c-p-c.lisp" (written 27 JAN 2024 04:18:09 AM): +. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-c-p-c.fasl +; compilation finished in 0:00:00.027 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-arglists.lisp" (written 27 JAN 2024 04:18:09 AM): +................. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-arglists.fasl +; compilation finished in 0:00:00.357 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-fuzzy.lisp" (written 27 JAN 2024 04:18:09 AM): +... + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-fuzzy.fasl +; compilation finished in 0:00:00.081 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-fancy-inspector.lisp" (written 27 JAN 2024 04:18:09 AM): +....... +.. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-fancy-inspector.fasl +; compilation finished in 0:00:00.125 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-presentations.lisp" (written 27 JAN 2024 04:18:09 AM): +. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-presentations.fasl +; compilation finished in 0:00:00.035 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-presentation-streams.lisp" (written 27 JAN 2024 04:18:09 AM): +. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-presentation-streams.fasl +; compilation finished in 0:00:00.022 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-asdf.lisp" (written 27 JAN 2024 04:18:09 AM): +..... + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-asdf.fasl +; compilation finished in 0:00:00.063 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-package-fu.lisp" (written 27 JAN 2024 04:18:09 AM): + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-package-fu.fasl +; compilation finished in 0:00:00.005 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-hyperdoc.lisp" (written 27 JAN 2024 04:18:09 AM): + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-hyperdoc.fasl +; compilation finished in 0:00:00.002 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-sbcl-exts.lisp" (written 27 JAN 2024 04:18:09 AM): + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-sbcl-exts.fasl +; compilation finished in 0:00:00.011 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-mrepl.lisp" (written 27 JAN 2024 04:18:09 AM): +......................................... +[package swank-api]............................... +[package swank-mrepl].. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-mrepl.fasl +; compilation finished in 0:00:00.035 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-trace-dialog.lisp" (written 27 JAN 2024 04:18:09 AM): +........................... +[package swank-trace-dialog]. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-trace-dialog.fasl +; compilation finished in 0:00:00.024 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-macrostep.lisp" (written 27 JAN 2024 04:18:09 AM): +..................... +[package swank-macrostep].. + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-macrostep.fasl +; compilation finished in 0:00:00.023 +; compiling file "/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-quicklisp.lisp" (written 27 JAN 2024 04:18:09 AM): + +; wrote /root/.slime/fasl/2.28/sbcl-2.0.1.debian-linux-x86-64/opt/quicklisp/dists/quicklisp/software/slime-v2.28/contrib/swank-quicklisp.fasl +; compilation finished in 0:00:00.002 + +* Removing intermediate container 2a0aec266b32 + ---> 1b37bc65403a +Step 8/16 : COPY myelin.asd . + ---> a6ec47991fc2 +Step 9/16 : COPY myelin.test.asd . + ---> 30ee7fbf3d5e +Step 10/16 : COPY src/*.lisp /opt/myelin/src/ + ---> b6fd69e824e6 +Step 11/16 : COPY tests/*.lisp /opt/myelin/tests/ + ---> 2017616aa0e2 +Step 12/16 : COPY integration-tests/*.lisp /opt/myelin/integration-tests/ + ---> ee2d67278cd8 +Step 13/16 : COPY scripts/dev-in-sandbox.lisp scripts/dev-in-sandbox.lisp + ---> e2d6061b5b58 +Step 14/16 : EXPOSE 4242 + ---> Running in 51980ee6ee90 +Removing intermediate container 51980ee6ee90 + ---> 0db072fe13c4 +Step 15/16 : EXPOSE 4005 + ---> Running in 42d149e97505 +Removing intermediate container 42d149e97505 + ---> 524a425f7732 +Step 16/16 : ENTRYPOINT ["sbcl", "--noinform", "--disable-ldb", "--lose-on-corruption", "--load", "scripts/dev-in-sandbox.lisp"] + ---> Running in f9187650da58 +Removing intermediate container f9187650da58 + ---> 1ee0837c4f2d +Successfully built 1ee0837c4f2d +Successfully tagged myelin:latest +docker run -it --rm --net=host myelin +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-test". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file. +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-dev". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file. +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/cl-who-20220331-git/cl-who.asd" contains definition for system "cl-who-test". Please only define "cl-who" and secondary systems with a name starting with "cl-who/" (e.g. "cl-who/test") in that file. +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/cl-strings-20210411-git/cl-strings.asd" contains definition for system "cl-strings-tests". Please only define "cl-strings" and secondary systems with a name starting with "cl-strings/" (e.g. "cl-strings/test") in that file. +ASDF could not load myelin because +Component QUOTE not found, required by +#. +While evaluating the form starting at line 2, column 0 + of #P"/opt/myelin/myelin.test.asd": +While evaluating the form starting at line 7, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a ASDF/FIND-COMPONENT:MISSING-DEPENDENCY in thread +#: + Component QUOTE not found, required by + # + + + +(ASDF/FIND-COMPONENT:RESOLVE-DEPENDENCY-NAME # QUOTE NIL) + error finding frame source: Bogus form-number: the source file has probably + changed too much to cope with. + source: NIL + + + + + + +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-test". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file. +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/hunchentoot-v1.3.0/hunchentoot.asd" contains definition for system "hunchentoot-dev". Please only define "hunchentoot" and secondary systems with a name starting with "hunchentoot/" (e.g. "hunchentoot/test") in that file. +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/cl-who-20220331-git/cl-who.asd" contains definition for system "cl-who-test". Please only define "cl-who" and secondary systems with a name starting with "cl-who/" (e.g. "cl-who/test") in that file. +WARNING: System definition file #P"/opt/quicklisp/dists/quicklisp/software/cl-strings-20210411-git/cl-strings.asd" contains definition for system "cl-strings-tests". Please only define "cl-strings" and secondary systems with a name starting with "cl-strin-gs/" (e.g. "cl-strings/test") in that file. + + + + + +; caught ERROR: +; READ error during COMPILE-FILE: +; +; Symbol "ADD-PACKAGE-LOCAL-NICKNAME" not found in the UIOP/DRIVER package. +; +; Line: 24, Column: 32, File-Position: 565 +; +; Stream: # + + + + + + + + +ASDF could not load myelin because +Failed to find the TRUENAME of /opt/myelin/src/project.lisp: + No such file or directory. +While evaluating the form starting at line 2, column 0 + of #P"/opt/myelin/myelin.test.asd": +While evaluating the form starting at line 7, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a SB-INT:SIMPLE-FILE-ERROR in thread +#: + Failed to find the TRUENAME of /opt/myelin/src/project.lisp: + No such file or directory + + + + + +This would be an "info", not a warning nor an error: +; wrote /root/.cache/common-lisp/sbcl-2.0.1.debian-linux-x64/opt/myelin/src/clock-tmpGHU3ALSV.fasl + + + + + +ASDF could not load myelin because The variable X is unbound.. +While evaluating the form starting at line 2, column 0 + of #P"/opt/myelin/myelin.test.asd": +While evaluating the form starting at line 7, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a UNBOUND-VARIABLE in thread +#: + The variable X is unbound. + + + + + + + + + +While evaluating the form starting at line 7, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a QUICKLISP-CLIENT:SYSTEM-NOT-FOUND in thread +#: + System "myelin.test" not found + + + + + + + + + +While evaluating the form starting at line 7, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a QUICKLISP-CLIENT:SYSTEM-NOT-FOUND in thread +#: + System "myelin.test" not found + + + + + + + + + + +While evaluating the form starting at line 10, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a SB-BSD-SOCKETS:ADDRESS-IN-USE-ERROR in thread +#: + Socket error in "bind": EADDRINUSE (Address already in use) + +Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. + +restarts (invokable by number or by possibly-abbreviated name): + 0: [USE-VALUE] Try a port other than 4005 + 1: [RETRY ] Retry EVAL of current toplevel form. + 2: [CONTINUE ] Ignore error and continue loading file "/opt/myelin/scripts/dev-in-sandbox.lisp". + 3: [ABORT ] Abort loading file "/opt/myelin/scripts/dev-in-sandbox.lisp". + 4: Ignore runtime option --load "scripts/dev-in-sandbox.lisp". + 5: Skip rest of --eval and --load options. + 6: Skip to toplevel READ/EVAL/PRINT loop. + 7: [EXIT ] Exit SBCL (calling #'EXIT, killing the process). + +(SB-BSD-SOCKETS:SOCKET-ERROR "bind" 98) + error finding frame source: Bogus form-number: the source file has probably + changed too much to cope with. + source: NIL +0] + + + + + + + + +WARNING: redefining MYELIN::EXTRACT-LINKS in DEFUN + +======= +While evaluating the form starting at line 168, column 0 + of #P"/opt/myelin/integration-tests/api-testing.lisp": +While evaluating the form starting at line 22, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a UNBOUND-VARIABLE in thread +#: + The variable *ADDRESS* is unbound. + +Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. + +restarts (invokable by number or by possibly-abbreviated name): + 0: [CONTINUE ] Retry using *ADDRESS*. + 1: [USE-VALUE ] Use specified value. + 2: [STORE-VALUE] Set specified value and use it. + 3: [RETRY ] Retry EVAL of current toplevel form. + 4: Ignore error and continue loading file "/opt/myelin/integration-tests/api-testing.lisp". + 5: [ABORT ] Abort loading file "/opt/myelin/integration-tests/api-testing.lisp". + 6: Retry EVAL of current toplevel form. + 7: Ignore error and continue loading file "/opt/myelin/scripts/dev-in-sandbox.lisp". + 8: Abort loading file "/opt/myelin/scripts/dev-in-sandbox.lisp". + 9: Ignore runtime option --load "scripts/dev-in-sandbox.lisp". + 10: Skip rest of --eval and --load options. + 11: Skip to toplevel READ/EVAL/PRINT loop. + 12: [EXIT ] Exit SBCL (calling #'EXIT, killing the process). + +(SB-INT:SIMPLE-EVAL-IN-LEXENV *ADDRESS* #) +0] + + + + + + + + + + +While evaluating the form starting at line 168, column 0 + of #P"/opt/myelin/integration-tests/api-testing.lisp": +While evaluating the form starting at line 22, column 0 + of #P"/opt/myelin/scripts/dev-in-sandbox.lisp": + +debugger invoked on a SB-INT:SIMPLE-PROGRAM-ERROR in thread +#: + invalid number of arguments: 1 + +Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. + +restarts (invokable by number or by possibly-abbreviated name): + 0: [REPLACE-FUNCTION] Call a different function with the same arguments + 1: [CALL-FORM ] Call a different form + 2: [RETRY ] Retry EVAL of current toplevel form. + 3: [CONTINUE ] Ignore error and continue loading file "/opt/myelin/integration-tests/api-testing.lisp". + 4: [ABORT ] Abort loading file "/opt/myelin/integration-tests/api-testing.lisp". + 5: Retry EVAL of current toplevel form. + 6: Ignore error and continue loading file "/opt/myelin/scripts/dev-in-sandbox.lisp". + 7: Abort loading file "/opt/myelin/scripts/dev-in-sandbox.lisp". + 8: Ignore runtime option --load "scripts/dev-in-sandbox.lisp". + 9: Skip rest of --eval and --load options. + 10: Skip to toplevel READ/EVAL/PRINT loop. + 11: [EXIT ] Exit SBCL (calling #'EXIT, killing the process). + +(SERVER-INTERFACE #) [external] + source: (DEFUN SERVER-INTERFACE () + "Get the interface the server is listening to" + (H:ACCEPTOR-ADDRESS *SERVER*)) +0] diff --git a/docs/.gitignore b/docs/.gitignore index 0b84df0f..d844968b 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,2 @@ -*.html \ No newline at end of file +*.html +org-roam.db \ No newline at end of file diff --git a/docs/aartaka.org b/docs/aartaka.org new file mode 100644 index 00000000..706a87b6 --- /dev/null +++ b/docs/aartaka.org @@ -0,0 +1,10 @@ +:PROPERTIES: +:ID: 52254263-fae9-4ead-8467-110735b07a2a +:END: +#+title: AArtaka + +a.k.a. Artyom Bologov + +https://github.com/aartaka/graven-image/tree/master + +https://github.com/aartaka/lisp-config/blob/master/ed.lisp diff --git a/docs/breeze_on_the_internets.org b/docs/breeze_on_the_internets.org new file mode 100644 index 00000000..b56c6319 --- /dev/null +++ b/docs/breeze_on_the_internets.org @@ -0,0 +1,28 @@ +:PROPERTIES: +:ID: b9f7e1f4-dc86-46e0-860b-f845f180110e +:END: +#+title: Breeze on the internets + +Keeping track of public discussions about breeze. +* Breeze on the internets + +** Lisp project of the day + +https://40ants.com/lisp-project-of-the-day/2020/08/0166-breeze.html + +** Reddit + +https://old.reddit.com/r/Common_Lisp/comments/pgtfm3/looking_for_feedbackhelp_on_a_project/ + +*** [[https://old.reddit.com/user/dzecniv][u/dzecniv]] + +> testing features along with workers and a file watcher? Shouldn't +they be different projects? + + What annoys you when developing in lisp? + +I find that setting up a test framework is more difficult than it +should be, so any effort on this area is appreciated. I mean: starting +with 5am is ok (but could be easier with an editor command), running +it from the CLI/a CI is less OK, getting the correct return code of +the tests needs more work, etc. diff --git a/bugs.txt b/docs/bugs.txt similarity index 100% rename from bugs.txt rename to docs/bugs.txt diff --git a/docs/change_impact_analysis.org b/docs/change_impact_analysis.org new file mode 100644 index 00000000..162aadcb --- /dev/null +++ b/docs/change_impact_analysis.org @@ -0,0 +1,26 @@ +:PROPERTIES: +:ID: f3e3952d-e6f7-4bb9-a85c-662ae82874eb +:END: +#+title: Change Impact Analysis + +I wanted to make breeze able to detect changes that haev impacts that +are hard for beginners to keep in mind (and easy to forget even for +experienced users). + +For example, you define a function, you rename it and re-evaluate the +defun. The source code only has the new function, but the image has +both the new and the old. It would be nice to have breeze help with +perhaps =fmakunbound= it or to update the package's exports (both in +the source and in the image). + +This is hard. + +But I just found a "keyword" that could help me find relevant +techniques: [[https://en.wikipedia.org/wiki/Change_impact_analysis][Change Impact Analysis (Wikipedia)]]. + +Furthermore, I found this "change impact analysis" while looking at +papers about "AST hashing", which is something I've had in my mind for +practically a decade, if not more. I did _some_ experiments around +this subject (AST hashing), but I was waiting for a better +reader/parser before trying something more fancy (than hashing nested +lists). diff --git a/docs/contributing.org b/docs/contributing.org new file mode 100644 index 00000000..02c84c30 --- /dev/null +++ b/docs/contributing.org @@ -0,0 +1,25 @@ +:PROPERTIES: +:ID: 279c4ea6-2004-4a7a-a2c9-905f27fae42c +:END: +#+title: Contributing + +* Contributing + +Start by forking and cloning this repository into quicklisp's +local-projects directory. + +Setup the pre-commit hook + +#+begin_src shell +git config core.hooksPath githooks +#+end_src + +Look for TODOs in the code + +#+begin_src shell +grep -ir --include='*.lisp' todo +# or +rg -i todo +#+end_src + +Explore the documentation. diff --git a/docs/design_decisions.org b/docs/design_decisions.org new file mode 100644 index 00000000..9b611a65 --- /dev/null +++ b/docs/design_decisions.org @@ -0,0 +1,72 @@ +:PROPERTIES: +:ID: 14d42b3a-0a2f-4a3b-8937-7175e621c6ec +:END: +#+title: Design Decisions + +* Design decisions + +** Write everything in common lisp + +As much as possible, so that breeze can easily be ported to different +platforms and editors. + +** Wrap definitions :obsolete: + +Decision: Create wrapper macros (e.g. =br:defun=) to keep the original +forms for later analysis. + +This decision is really not definitive. + +This decision is less than ideal, especially for existing systems, but +it was the easiest to start with. + +*** Alternatives + +**** Keep the string being eval'd + +Advising swank's eval function is "a good start" in that direction. + +**** Parse the source code + +- Might be hard, but [[https://github.com/s-expressionists/Eclector][eclector]] could make this easy. +- [[https://github.com/hyotang666/read-as-string][hyotang666/read-as-string]] is another candidate + +** Migrate to parachute 2022-03-08 + +The test framework and the "wrap definition" parts always were +proof-of-concepts: I wanted to be able to define some tests, and run +them when either the test of the system-under-test was redefined. It +worked, but now that I have a more and more complete common lisp +parser, I can do the things properly. So I've move the concerned code +into the folder "scratch-files" and I'll re-introduce them slowly in +the future. (Because I really want something to run the tests in the +background, for example.) + +** Read from strings instead of streams + +I did some tests and the code was like 100x faster when reading from +string instead of reading from streams. There are multiple reasons: to + extract the "raw" text from the stream require consing new strings +_and_ abusing file-position to move back and forth in the stream, both +of these are very inefficient. Instead, we use displaced arrays which +results in way less consing and no "stream state" to manage. This made +both the code faster and simpler. + +From another point of view: why not? we were already copying the whole +stream into the resulting tree, now we just have references to one +string. + +** Use =licence= and not =license= + +This is a very tiny decision, but I know I'll forget it. + +What made me decide between the two: =licence= is what asdf use, and +it's what the user will see in their projects. + +** Only use dependencies from quicklisp's distribution + +This project is not in quicklisp, and I don't plan to add it to +quicklisp until it stabilizes (which might take years). I make sure to +only use dependencies from quicklisp so that if somebody wants to try +it out they'll just need to clone this repository in quicklisp's +local-projects folder. diff --git a/docs/e_graphs.org b/docs/e_graphs.org new file mode 100644 index 00000000..3788e53f --- /dev/null +++ b/docs/e_graphs.org @@ -0,0 +1,26 @@ +:PROPERTIES: +:ID: 32155195-1bc4-4f2d-8f6a-12fb0bd68ecc +:END: +#+title: E-graphs + +* Introduction + +An e-graph is a data structure that can represent an exponential (or +even infinite, because of loops) number of forms in a polynomial +amount of space. + +They are a specific case of finite tree automata and are closely +related to version-state algebras (VSAs). + +* In common lisp + +I have a proof of concept implementation in common lisp here: +https://gitlab.com/fstamour/catchall/-/tree/master/egraph + +* References + +- https://egraphs-good.github.io/ +- https://colab.research.google.com/drive/1tNOQijJqe5tw-Pk9iqd6HHb2abC5aRid?usp=sharing +- https://arxiv.org/pdf/2004.03082.pdf + +** TODO Find the paper relating FTA, VSAs and e-graphs diff --git a/docs/eclector.org b/docs/eclector.org new file mode 100644 index 00000000..d98e450c --- /dev/null +++ b/docs/eclector.org @@ -0,0 +1,8 @@ + + + +#+begin_src lisp +(eclector.parse-result:read + (make-instance 'skipped-input-recording-client) + stream nil :eof) +#+end_src diff --git a/docs/editor_integrations.org b/docs/editor_integrations.org new file mode 100644 index 00000000..69010b18 --- /dev/null +++ b/docs/editor_integrations.org @@ -0,0 +1,18 @@ +:PROPERTIES: +:ID: 5d211d9a-0749-4adb-abe0-e66133d09b5b +:END: +#+title: Editor integrations + +- [[id:6bd2b06d-0a3c-4d32-9a1e-4f6f36e1003d][Emacs integration]] +- [[id:086c7705-e5ec-4dc0-852d-211c055eb145][Visual Studio Code integration]] (not implemented) + +* TODO Make sure the commands are executed against _common lisp_ code :ux:editor: + +Gavinok tried breeze, but ran ~C-.~ on ~breeze.el~, this caused +confusion for everybody. + +Here's some ideas to help with this: + +- check the extension of the buffer or file +- check the mode of the buffer +- make sure it's not in the repl (for now) diff --git a/docs/elpy.org b/docs/elpy.org new file mode 100644 index 00000000..e089b1c3 --- /dev/null +++ b/docs/elpy.org @@ -0,0 +1,13 @@ +:PROPERTIES: +:ID: 5265bce5-c6d0-4cda-8d3e-699ceafcab42 +:END: +#+title: Elpy + +#+begin_quote +Elpy is an extension for the Emacs text editor to work with Python +projects. +#+end_quote + +https://elpy.readthedocs.io/en/latest/ + +- Uses [[id:5eb1faac-b7b5-4c99-abe9-b91e77bea4ae][Jedi]] diff --git a/docs/emacs.md b/docs/emacs.md deleted file mode 100644 index e8be2e9b..00000000 --- a/docs/emacs.md +++ /dev/null @@ -1,22 +0,0 @@ -# Emacs integration - -## Features - -* Snippets -* Capture ideas easily (`breeze-capture`) -* Create project interactively -* Refactor -* Provides a minor-mode `breeze-mode` - -## Setup - - (load `quicklisp/local-projects/breeze/src/breeze.el`) - (add-hook 'lisp-mode-hook #'breeze-mode) - -## Commands and default keymap - -| Command | Description | Default Key | -| - | - | - | -| breeze-quickfix | Choose from a list of commands applicable to the current context. | `C-.` | -| breeze-capture | Quickly create a lisp file in a pre-determined directory. | | -| breeze-scaffold-project | Interactively create a project in `quicklisp`'s `local-projects` folder using `quickproject`. | | diff --git a/docs/emacs_eglot.org b/docs/emacs_eglot.org new file mode 100644 index 00000000..f38b8856 --- /dev/null +++ b/docs/emacs_eglot.org @@ -0,0 +1,7 @@ +:PROPERTIES: +:ID: 38d6dbd7-0580-4701-bd52-ee97174a0535 +:END: +#+title: Emacs' eglot + +- =eglot--propose-changes-as-diff= +- =eglot-forget-pending-continuations= diff --git a/docs/emacs_integration.org b/docs/emacs_integration.org new file mode 100644 index 00000000..073314c9 --- /dev/null +++ b/docs/emacs_integration.org @@ -0,0 +1,27 @@ +:PROPERTIES: +:ID: 6bd2b06d-0a3c-4d32-9a1e-4f6f36e1003d +:END: +#+title: Emacs integration + +* Features + +- Snippets +- Capture ideas easily (`breeze-capture`) +- Create project interactively +- Refactor +- Provides a minor-mode `breeze-mode` + +* Setup + +#+begin_src emacs-lisp +(load `quicklisp/local-projects/breeze/src/breeze.el`) +(add-hook 'lisp-mode-hook #'breeze-mode) +#+end_src + +* Commands and default keymap + +| Command | Description | Default Key | +|-------------------------+-----------------------------------------------------------------------------------------------+-------------| +| breeze-quickfix | Choose from a list of commands applicable to the current context. | `C-.` | +| breeze-capture | Quickly create a lisp file in a pre-determined directory. | | +| breeze-scaffold-project | Interactively create a project in `quicklisp`'s `local-projects` folder using `quickproject`. | | diff --git a/docs/error_recovery.org b/docs/error_recovery.org new file mode 100644 index 00000000..5bea0e7a --- /dev/null +++ b/docs/error_recovery.org @@ -0,0 +1,12 @@ + +Error recovery + +- basic: use "synchronization points", places where it looks like a +good place to restart parsing after an invalid parse + +- it would be much easier to pin-point the source of the failure if we +start from a previous good state (incremental parsing) + +- I noticed that for lisp, a lot of things would be "easy" to parse +backward, this would help tremendously pin-pointing where a "bad +parse" begins. diff --git a/docs/faq_from_newbies_about_common_lisp.org b/docs/faq_from_newbies_about_common_lisp.org new file mode 100644 index 00000000..6ffa81fb --- /dev/null +++ b/docs/faq_from_newbies_about_common_lisp.org @@ -0,0 +1,79 @@ +:PROPERTIES: +:ID: 31236780-159e-4a58-9019-37f57f5b4997 +:END: +#+title: FAQ from newbies about common lisp + +This is useful for breeze's development, to figure out the pain +points. Ideally, this should go somewhere else, like in tutorial, a +cookbook or some kind of reference (like [[https://github.com/fstamour/lisp-docs.github.io][lisp-docs.github.io]]). + +* FAQ from newbies about common lisp + +** What's the difference between load and require? + +** What's asdf v. quicklisp v. packages v. "os packages"? + +** The heck is RPLACA? + +** What's the difference between =setf= and =setq=? + +https://stackoverflow.com/questions/869529/difference-between-set-setq-and-setf-in-common-lisp + +** Why use #:symbol (especially in =defpackage=)? + +** Why start a file with =(cl:in-package #:cl-user)=? + +** Why interactivity is important? + +They don't actually ask that, they usually just don't think or know +about it. + +Here's something that does an OK job at explaining the importance: +https://technotales.wordpress.com/2007/10/03/like-slime-for-vim/ + +** What's the difference between ~defvar~ and ~defparameter~? + +** Something about using ~setf~ to create variables... + +** A symbol can represent many things + +- variables/symbol macros +- functions/macros +- classes/conditions/types +- method combinations +- block names +- catch tags +- tagbody tags +- restarts +- packages +- compiler macros +- slot names +- compiler macros + +** When coming from another language + +*** How to create a function-local variable? + +** Proclaim v.s. Declaim v.s. Declare + +http://www.lispworks.com/documentation/lw50/LWUG/html/lwuser-90.htm + +** How packages and symbols works? + +https://flownet.com/ron/packages.pdf + +** Alternatives to the Hyperspec + +- [[http://clqr.boundp.org/download.html][Common Lisp Quick Reference]] +- Ultraspec (dead) +- Simplified something something +- The lisp cookbook + +** What the hell are pathnames? + +- Don't forget trailing backslashes for directories. + +** Where are the functions to operate on strings? + +- Use the functions that operate on sequences. +- Use libraries, like alexandria, split-sequences, serapeum, etc. diff --git a/docs/features.org b/docs/features.org new file mode 100644 index 00000000..1acfe0d6 --- /dev/null +++ b/docs/features.org @@ -0,0 +1,7 @@ +:PROPERTIES: +:ID: 13ea055e-4715-4583-811b-bff78ca300ee +:END: +#+title: Candidate Features + +- [[id:5d211d9a-0749-4adb-abe0-e66133d09b5b][Editor integrations]] +- [[id:54e6cd55-803b-4e15-82bc-a332130d020e][Sly/Slime integration]] diff --git a/docs/file_watching.org b/docs/file_watching.org new file mode 100644 index 00000000..5fa708b1 --- /dev/null +++ b/docs/file_watching.org @@ -0,0 +1,16 @@ +:PROPERTIES: +:ID: 373c4a22-b450-40e1-8d00-1319e0277b68 +:END: +#+title: File watching +* Portable file watching + +https://www.reddit.com/r/lisp/comments/1iatcd/fswatcher_watches_filesystem_changes/ + +http://eradman.com/entrproject/ + +https://github.com/Ralt/fs-watcher (polls) + +https://github.com/Shinmera/file-notify <=== + +2023-09-25 I briefly talked with Shinmera this summer, and they +mentioned that this project doesn't currently work. diff --git a/docs/flymake.org b/docs/flymake.org new file mode 100644 index 00000000..66f3d841 --- /dev/null +++ b/docs/flymake.org @@ -0,0 +1,13 @@ +:PROPERTIES: +:ID: 5b8f3aee-0ea4-4688-8de7-e0b3ac140405 +:END: +#+title: Flymake + +flymake-show-buffer-diagnostics +flymake-show-project-diagnostics + +next-error-function + +- https://www.gnu.org/software/emacs/manual/html_node/flymake/Troubleshooting.html +- https://www.gnu.org/software/emacs/manual/html_node/flymake/Backend-functions.html +- https://www.gnu.org/software/emacs/manual/html_node/flymake/An-annotated-example-backend.html diff --git a/docs/formatting_code.org b/docs/formatting_code.org new file mode 100644 index 00000000..a35bb96c --- /dev/null +++ b/docs/formatting_code.org @@ -0,0 +1,9 @@ +:PROPERTIES: +:ID: d487821d-01e7-41a1-b9db-1a856fd7eb01 +:END: +#+title: Formatting code + +https://github.com/radian-software/apheleia + +🌷 Run code formatter on buffer contents without moving point, using +RCS patches and dynamic programming. diff --git a/docs/getting_started.org b/docs/getting_started.org new file mode 100644 index 00000000..77444d38 --- /dev/null +++ b/docs/getting_started.org @@ -0,0 +1,44 @@ +:PROPERTIES: +:ID: 306350c9-0fb5-478b-958b-b35cae726280 +:END: +#+title: Getting Started + +* Clone + +Clone this repository in quicklisp's local-projects folder. + +#+begin_src shell +git clone git@gitlab.com:fstamour/breeze.git ~/quicklisp/local-projects/breeze +#+end_src + +* Load in emacs + +#+begin_src emacs-lisp +(add-to-list 'load-path "~/quicklisp/local-projects/breeze/src/") +(require 'breeze) +#+end_src + +* Configure emacs to use breeze-minor-mode + +Add a hook to enable breeze's minor mode in =lisp-mode= automatically: + +The mode will add some bindings (most notably =C-.=, which is bound to +the command =breeze-quickfix=). + +#+begin_src emacs-lisp +(add-hook 'lisp-mode-hook #'breeze-minor-mode) +#+end_src + +Additionally, breeze can be use as a "on-the-fly" linter for common +lisp source files by enabling flymake and configuring flymake to use +breeze. + +#+begin_src emacs-lisp +;; Enable flymake whenever breeze-minor-mode is enabled +(add-hook 'breeze-minor-mode-hook 'flymake-mode) +;; Configure flymake whenever breeze-minor-mode is enabled +(add-hook 'breeze-minor-mode-hook 'breeze-setup-flymake-backend) +;; Configure eldoc to show both documentation and flymake's messages +;; See https://www.masteringemacs.org/article/seamlessly-merge-multiple-documentation-sources-eldoc for a fancier solution +(setf eldoc-documentation-strategy 'eldoc-documentation-compose) +#+end_src diff --git a/docs/glossary.org b/docs/glossary.org new file mode 100644 index 00000000..17e75436 --- /dev/null +++ b/docs/glossary.org @@ -0,0 +1,37 @@ +:PROPERTIES: +:ID: bb5c6ad4-0f89-48aa-9295-13e5e248a897 +:END: +#+title: Glossary +* Glossary + +#+begin_comment +Trying to document the words/concepts used in the project. +#+end_comment + +** lisp listener +:PROPERTIES: +:ID: 93da5b9d-9593-45b1-9f71-f49d01c3e95d +:END: + +- More often called "lisp repl". +- I use this term to try to avoid confusion with an hypothetical + future actual REPL. +- You could describe that as a "client-server REPL". + +** REPL +:PROPERTIES: +:ID: 824a7d5d-d11f-40b0-ae0e-b83ea7dbf812 +:END: + +- Stands for "Read-Eval-Print-Loop" +- Most people think about "command line" when they hear REPL, but in + the case of lisp, it usually means a "listener". + +** e-graph + +"e-graph" stands for "equivalence graph", it is a data structure. + +See [[id:32155195-1bc4-4f2d-8f6a-12fb0bd68ecc][E-graphs]]. + + +** TODO Package-local nicknames (PLN) diff --git a/docs/goals.org b/docs/goals.org new file mode 100644 index 00000000..13240eab --- /dev/null +++ b/docs/goals.org @@ -0,0 +1,19 @@ +:PROPERTIES: +:ID: e5d64314-8b13-4a6b-997f-1aae94910d63 +:END: +#+title: Goals and non-goals + +* Goals + +- Make it easier to develop in common lisp + - by any means +- With any editor (or even without one) +- Be as portable as possible +- Be useful to new and experimented developer (or even + non-developpers, we'll get there) + +* Non-goals + +- Replace slime, sly, slimv, slima, etc +- Replace existing test framework +- Force the user to use a set of conventions diff --git a/docs/ideas.org b/docs/ideas.org new file mode 100644 index 00000000..58caf969 --- /dev/null +++ b/docs/ideas.org @@ -0,0 +1,110 @@ +:PROPERTIES: +:ID: e2ff6189-1fd8-4d3c-9b7d-3d3ddbf2b0aa +:END: +#+title: Ideas +* Random ideas +** (tips), (tips "test"), (tips "doc") +** (next) ;; what's next? print functions that aren't done, that have no tests or documentation. +*** functions that aren't implemented or done +*** functions that have no tests +*** functions that have no documentation +*** Have a plain user-controlled task list +** Evaluate quality of documentation +*** e.g. if the documentation is almost just the name of the function +*** Make sure it doesn't "only" refer to another function +*** It's more that the content of the function + +(defun print-x (x) + "print (* x x)" + (print (* x x)) + +*** Make sure that all package have a :documentation +*** Make sure that all classes have a :documentation +** Evaluation the quality of the code +*** Cyclomatic complexity +*** Length of variable names +*** linting in general +** Compare the files in a system's directory and the actual components. +** See BIST to probalistically compare functions +*** Use a PRNG to generate inputs, use a hash to fingerprint the outputs +See [[file:scratch-files/function-fingerprinting.lisp][function-fingerprinting.lisp]] + +** Generate test for existing functions + +- The more we know the types of the expression, the more we can narrow + down the search. +- It would be easier if we knew which expression are safe to execute + +** Generate code based on desired input/output + +https://github.com/webyrd/Barliman + +- The more we know the types of the expression, the more we can narrow + down the search. +- It would be easier if we knew which expression are safe to execute +- The linter can help choose better results +- Using e-graph to refactor candidates can help suggest helper + functions + + +*** See Programming by examples (inductive synthesis) + +** A lot of things could be done by instrumenting the code + +Which is one of the reason behind wrapping the definitions (e.g. =breeze:defun=) + +- fault injection +- program (dynamic) slicing +- Stepping though code +- profiling +- test coverage +- coverage guided +- profile-guided optimization + +** Program slicing + +*** For code navigation + +It would be nice to be able to search for something (e.g. calls to +make-instance) only in a certain slice (e.g. from the "call tree" of +foo). + +*** Correlate with unit tests + +If we have multiple tests on the same piece of code, we can use the +slices from the tests that pass and the tests that fail to narrow down +which slice is probably the source of the failure. + +** Use equivalence-graph e-graph to suggest refactors + +Main resource: [[https://egraphs-good.github.io/][E-Graphs Good]] + +This might be hard and complicated, I was thinking that I should start +by making this work on a very small scope. For example, if the user +ask to suggest some refactors, we can look for forms that contains +only arithmetic (again, just an example) and nothing else, that use +equality saturation to find interesting equivalent forms and propose +them to the user. + +*** Small discussion I had on lobste.rs about e-graphs on lisp + +- [[https://lobste.rs/s/myyznl/tooling_for_tooling#c_apjopu][Comment on Lobste.rs]] + +The important bit: + +#+begin_quote +egg is great for algebraic rewrites, but doesn’t have good builtin +tools for associative/commutative operations, nor for +alpha-equivalence and rewriting under binders. I deliberately left +names out of my syntax so that rewriting would be easier; this won’t +be as simple for Lisps in general. +#+end_quote + +*** I already started working on implementing equivalence graphs + +A while ago I started by writing a disjoint sets data structure (also +known as union-find, based on the 2 mains operations it supports). + +https://github.com/fstamour/disjoint-sets + +** Semantic diffs using breeze.reader diff --git a/docs/improve_cl_docstring_s_at_runtime.org b/docs/improve_cl_docstring_s_at_runtime.org new file mode 100644 index 00000000..979aef6a --- /dev/null +++ b/docs/improve_cl_docstring_s_at_runtime.org @@ -0,0 +1,10 @@ +:PROPERTIES: +:ID: 9dbbf418-de72-4d31-8347-19e3dc7d8df1 +:END: +#+title: Improve CL docstring's at runtime + +I noticed that, on sbcl, you can =(setf (documentation 'x 'function) +...= on symbols that are part of the =cl= package (which I didn't +expect because of the package lock). It _could_ be nice to load a +package during development that adds examples to the docstrings, and +perhaps even "links" between the defintions? diff --git a/docs/inbox.org b/docs/inbox.org new file mode 100644 index 00000000..6d0969c0 --- /dev/null +++ b/docs/inbox.org @@ -0,0 +1,169 @@ +:PROPERTIES: +:ID: 598a884c-56d0-4378-b5f5-acb2671d5112 +:END: +#+title: Inbox + +#+begin_quote +This contains the notes that needs to be categorized/cleaned up. +#+end_quote + +* Prior Arts + +** TODO This is just a dump of links, I need at least a small description and reason why it might be useful + +** Tinker (1980) + +http://web.media.mit.edu/%7Elieber/Lieberary/Tinker/Tinker/Tinker.html + +** Image Based development + +[Image based development](https://www.informatimago.com/develop/lisp/com/informatimago/small-cl-pgms/ibcl/index.html) + +** Code refactoring tools and libraries, linters, etc. + +*** General + +https://comby.dev/ (and https://github.com/s-kostyaev/comby.el) +https://github.com/reviewdog/reviewdog + +*** common lisp + +https://github.com/hyotang666/trivial-formatter +https://github.com/yitzchak/cl-indentify +https://github.com/vindarel/colisper (uses comby) + - its catalog of rewrites: https://github.com/vindarel/colisper/tree/master/src/catalog/lisp +https://github.com/cxxxr/sblint +https://github.com/g000001/lisp-critic/ +https://github.com/eschulte/lisp-format + +*** javascript and front-end in general + +https://github.com/facebookarchive/codemod replaced by +https://github.com/facebook/jscodeshift, which uses +https://github.com/benjamn/recast + +Examples: https://github.com/cpojer/js-codemod + +*** Ruby + +https://github.com/whitequark/parser +https://github.com/seattlerb/ruby_parser +https://github.com/seattlerb/ruby2ruby/ +https://docs.rubocop.org/rubocop-ast/node_pattern_compiler.html +https://nodepattern.herokuapp.com/ +https://github.com/mbj/unparser + +*** Other + +Probably Rosely for C# and clang for C/C++. I'm sure there are tons of +tools/libraries for Java. + +For python, there's the ast module, but I don't know if it can +preserve the formatting. There's a bunch of tools to format the code. + +** Zulu.inuoe's attempt - clution + +- https://github.com/Zulu-Inuoe/clution +- https://github.com/Zulu-Inuoe/clution.lib +- https://github.com/Zulu-Inuoe/lob + +* Protocols + +- [[https://chromedevtools.github.io/devtools-protocol/][Chrome DevTools Protocol]] +- Slime/Sly +- LSP (Language Server Protocol) +- LSIF (Language Server Index Format) +- Debug Adapter Protocol + +* To classify + +https://quickdocs.org/cl-scripting +https://quickdocs.org/repl-utilities +[[https://github.com/slime/slime/issues/532][slime issue #532: Rename package and all the symbol prefixes]] +https://blog.cddr.org/posts/2021-11-23-on-new-ides/ +https://common-lisp.net/project/slime/doc/html/Contributed-Packages.html + +https://quickdocs.org/external-symbol-not-found +https://github.com/Bike/compiler-macro +https://quickdocs.org/dotenv + +https://github.com/tdrhq/slite +https://quickdocs.org/slite - SLIME based Test-runner for FiveAM tests +(and possibly others in the future) + +In SLIME's debugger, press ~v~ to navigate to its definition. + +https://github.com/melisgl/journal - for logging and trace-based +testing +https://github.com/melisgl/try/ - for a test framework that looks a +lot with what I want from a test framework. + +For a pretty nice review of existing testing framework: +https://sabracrolleton.github.io/testing-framework + +[[https://github.com/emacs-elsa/Elsa][Emacs Lisp Static Analyzer]] + +https://github.com/ruricolist/moira - Monitor and restart background threads. + +https://github.com/pokepay/cl-string-generator - Generate string from regular expression + +Emacs supports ~(declare (pure t) (side-effect-free t))~ + +[[https://github.com/programingship/common-lisp-sly][Sly with spacemacs]] + + +https://github.com/mmontone/duologue - High level user interaction library for Common Lis + +* Discord + +Discussion about =#:= +https://discord.com/channels/297478281278652417/569524818991644692/915330555334234192 + +* SICL + +A fresh implementation of Common Lisp +https://github.com/robert-strandh/SICL + +I'm sure there are tons of other user-case: +- infer types +- interpret code (symbolically or not) + +* How froute uses mop to keep track of a set of definitions + +#+begin_comment +Maybe I should have a section about "code snippets that could be useful"? +#+end_comment + +[[https://github.com/StephenWakely/froute/blob/3d9ea3114537e1451cccec91f7cbe2321a49a1e0/src/froute-class.lisp][froute-class.lisp]] + +* See + +** DONE uses locative: http://quickdocs.org/mgl-pax/ :editor: + +2022-03-17 - I read most of the readme, this system looks awesome + +It's mostly for documentation, but it also expand slime/swank for +easier navigation (using the concept of locative). + +** DONE CCL's Watches https://ccl.clozure.com/manual/chapter4.12.html#watched-objects :editor: + +> Clozure CL provides a way for lisp objects to be watched so that a +condition will be signaled when a thread attempts to write to the +watched object + +Very useful for debugging. + +** DONE CCL's Advise https://ccl.clozure.com/manual/chapter4.3.html#Advising + +> The advise macro can be thought of as a more general version of +trace. + +I think I kept this link just for the general interface (~advise~, +~unadvise~ and ~advisep~) + +* CLI / TUI + +Gum is getting more and more interesting, perhaps I could try using it +as a prototype (just like I used emacs to bootstrap this project). + +https://github.com/charmbracelet/gum diff --git a/docs/index.org b/docs/index.org new file mode 100644 index 00000000..67724f24 --- /dev/null +++ b/docs/index.org @@ -0,0 +1,33 @@ +:PROPERTIES: +:ID: 9c910250-abdc-4cbe-961b-46ad5c4f82d4 +:END: +#+title: Breeze +#+options: toc:nil + +* Documentation + +- [[id:d08ab932-1204-4e7c-9869-40fc53500071][Introduction]] +- [[id:e5d64314-8b13-4a6b-997f-1aae94910d63][Goals and non-goals]] +- [[id:11dd9906-75ff-4abc-82a5-b7dda0936f06][Roadmap]] +- [[id:306350c9-0fb5-478b-958b-b35cae726280][Getting Started]] +- [[id:5d211d9a-0749-4adb-abe0-e66133d09b5b][Editor integrations]] +- [[id:14d42b3a-0a2f-4a3b-8937-7175e621c6ec][Design Decisions]] +- [[id:279c4ea6-2004-4a7a-a2c9-905f27fae42c][Contributing]] +- [[id:e712f3d1-0734-43f0-886a-3008ca5f722d][Testing Breeze]] +- [[id:bb5c6ad4-0f89-48aa-9295-13e5e248a897][Glossary]] +- [[file:reference.html][Reference]] +- [[file:listing-breeze.html][Source listings]] + +* Internal notes + +- [[id:7d0f5cd2-d216-4882-84ac-27c004ad6fbd][Links to some dependencies' documentation]] +- [[id:598a884c-56d0-4378-b5f5-acb2671d5112][Inbox]] +- [[id:e2ff6189-1fd8-4d3c-9b7d-3d3ddbf2b0aa][Ideas]] +- [[id:b9f7e1f4-dc86-46e0-860b-f845f180110e][Breeze on the internets]] +- [[id:31236780-159e-4a58-9019-37f57f5b4997][FAQ from newbies about common lisp]] +- [[id:62112623-6002-4cb9-87de-cb530ce0a36e][Silimar projects]] +- [[id:13ea055e-4715-4583-811b-bff78ca300ee][Candidate Features]] + +** More specific notes + +- [[id:32155195-1bc4-4f2d-8f6a-12fb0bd68ecc][E-graphs]] diff --git a/docs/internals.md b/docs/internals.md deleted file mode 100644 index 82a8eef3..00000000 --- a/docs/internals.md +++ /dev/null @@ -1,100 +0,0 @@ -# TODO This is _waaay_ out of date - -```mermaid -sequenceDiagram - user-->>+breeze.el: User call a command in the editor - breeze.el-->>+breeze.command: Send command, buffer and metadata - loop - breeze.command-->>-breeze.el: Tell the editor what to do - breeze.el-->>+breeze.command: Editor returns results - end - breeze.command-->>-breeze.el: Tell the editor that it's done - breeze.el-->>-user: The editor command is done -``` - - -```mermaid -sequenceDiagram - autonumber - - participant ed as editor - - participant start as start-command - participant cont as continue-command - participant run as run-command - - participant out as channel-out - participant in as channel-in - - participant th as command thread - participant cmd as command function - - - Note over start, run: These functions run in a REPL thread - Note over in, out: These are channels to communicate
with the command thread - - Note over ed, cmd: Initialization - ed->>+start: start the command - start-->>start: Set *current-command* - start-)th: start the thread - - par - start->>+in: send the tasklet (thread + channels) - in-->>-start: - start->>+out: wait for the start message - out-->>-start: - and - in->>+th: wait for tasklet - th-->>-in: - th-->>th: (setf (command-tasklet *current-command*) ...) - th->>+out: send :started - out-->>-th: - th->>+cmd: call command - end - - - Note over ed, cmd: First run - start->>+run: call run-command - - par - cmd->>+out: send request - out-->>-cmd: - and - run->+out: wait for the request - out-->>-run: - end - - run-->>-start: return the request - start-->>-ed: send the request to the editor - - Note over ed, cmd: Loop until the request "done" is sent - loop - ed-->>ed: process the request - ed->>+cont: call continue-command - Note over cont: This is probably in a different REPL thread - cont->>+run: call run-command - - opt if response expected is last command - par - run->>+in: send response from editor - in-->>-run: - and - cmd->>+in: wait for the response - in-->>-cmd: - end - end - - par - cmd->>+out: send new request - out-->>-cmd: - and - run->>+out: wait for a new request - out-->>-run: - end - - run-->>-cont: return new request - cont-->>-ed: send new request to editor - end - - cmd-->>-th: return to command-thread's lambda -``` diff --git a/docs/introduction.org b/docs/introduction.org new file mode 100644 index 00000000..732d5da7 --- /dev/null +++ b/docs/introduction.org @@ -0,0 +1,52 @@ +:PROPERTIES: +:ID: d08ab932-1204-4e7c-9869-40fc53500071 +:END: +#+title: Introduction + +* What is this? + +This is a git repository that contains lots of common lisp code that I +use to make developing with common lisp easier. It is a personal +project that I work on from time to time, but that I use (and break) +pretty much all the time. + +* Features + +- Emacs integration +- Integration with quickproject +- Context-aware, configurable snippets and refactorings +- Command for quick code capture (trying out code in a new file) +- Implemented in common lisp to be able to port it to other editors in + the future + +Currently, breeze's main interface is emacs; `breeze.el` adds a few +commands and one minor-mode with one bindings (`C-.`). + +The one binding calls a command named `breeze-quickfix` (might rename +in the future). This command suggests applicable actions given the +current context (file name, file content, position in the file, +etc.). For example, if the file ends with ".asd" it will suggest a +command to insert a `defsystem` form. If breeze was already +configured, it will pre-fill the `:maintainer`, `:author` and +`:licence` fields. Another example is that if the file is empty, or +contains only comments, it will suggest to insert a `defpackage` or +`uiop:define-package` form. It is also able to detect when you're +trying to edit/evaluate forms that are in a package that doesn't +exists (_did you forget to evaluate the `defpackage` form_). + +The integration with quickproject is pretty simple and let's you +quickly create new projects from the comfort of your editor. The +integration consists of one command that asks you for some +information, like the project name and licence. It takes some default +values from breeze's configuration, but let's you change them. All +this to ease the use of quickproject. + +Another simple command that helps me is `breeze-capture`, it creates a +new file in a pre-determined (must be configured) folder and fills it +with some pre-configured content (template) and let's you code right +away. This could've easily be done in emacs (that's how I prototyped +the first version), but doing this in common lisp makes it easy to +port it to other editors (or just the repl) in the future. + +I must stress that this whole project is in constant flux, and until I +add more and more tests, stuff might break any time. diff --git a/docs/jedi.org b/docs/jedi.org new file mode 100644 index 00000000..b989e963 --- /dev/null +++ b/docs/jedi.org @@ -0,0 +1,11 @@ +:PROPERTIES: +:ID: 5eb1faac-b7b5-4c99-abe9-b91e77bea4ae +:END: +#+title: Jedi + +#+begin_quote +Jedi - an awesome autocompletion, static analysis and refactoring +library for Python +#+end_quote + +https://jedi.readthedocs.io/en/latest/ diff --git a/docs/language_server_protocol.org b/docs/language_server_protocol.org new file mode 100644 index 00000000..361d8517 --- /dev/null +++ b/docs/language_server_protocol.org @@ -0,0 +1,8 @@ +:PROPERTIES: +:ID: 9d5bc298-56d4-40c4-af2e-5b127d5914bf +:END: +#+title: Language Server Protocol +* TODO An implementation of the Language Server Protocol for Common Lisp :editor: + +- https://github.com/cxxxr/cl-lsp +- related: https://marketplace.visualstudio.com/items?itemName=ailisp.commonlisp-vscode diff --git a/docs/links_to_some_dependencies_documentation.org b/docs/links_to_some_dependencies_documentation.org new file mode 100644 index 00000000..af1ac4a2 --- /dev/null +++ b/docs/links_to_some_dependencies_documentation.org @@ -0,0 +1,7 @@ +:PROPERTIES: +:ID: 7d0f5cd2-d216-4882-84ac-27c004ad6fbd +:END: +#+title: Links to some dependencies' documentation +* Resources + +- [[https://alexandria.common-lisp.dev/draft/alexandria.html][alexandria]] diff --git a/docs/linting_asd_files.org b/docs/linting_asd_files.org new file mode 100644 index 00000000..0a666d9d --- /dev/null +++ b/docs/linting_asd_files.org @@ -0,0 +1,20 @@ +:PROPERTIES: +:ID: e55cab7e-beb6-4cb4-be2e-d0d78a8f568a +:END: +#+title: Linting asd files + +* Use =(in-package #:asdf-user)= at the start of the file + +Using (interned) symbols in the .asd files _might_ intern symbols in +=*package*= (especially if someone tries to =(read ...)= the =.asd= +file. + +* Look for missing files + +I could see one nice workflow: +1. Open the system defintion +2. Add a file in the =:components= list +3. The linter highlights the missing file +4. The user choose a "code action" that creates the file and opens it + in the editor, perhaps even adding a package definition at the + start. diff --git a/docs/listener_features.org b/docs/listener_features.org new file mode 100644 index 00000000..0185f3bd --- /dev/null +++ b/docs/listener_features.org @@ -0,0 +1,9 @@ +:PROPERTIES: +:ID: d21da464-7b9e-47d2-bc2a-c9ab7a927218 +:END: +#+title: Listener features + +condition "package not found" => +- try to fix spelling +- load a system or file that contains the package +- create a new system or file to create the diff --git a/docs/parsing_common_lisp.org b/docs/parsing_common_lisp.org new file mode 100644 index 00000000..172b49c7 --- /dev/null +++ b/docs/parsing_common_lisp.org @@ -0,0 +1,14 @@ +:PROPERTIES: +:ID: edbd3cb1-e04b-41f9-b35f-20c123854481 +:END: +#+title: Parsing Common Lisp + +* Concrete Syntax Tree +:PROPERTIES: +:ID: 1f979dc4-a4b7-4223-af54-82fa3725c8a3 +:END: + +https://github.com/s-expressionists/Concrete-Syntax-Tree + +This library is intended to solve the problem of source tracking for +Common Lisp code. diff --git a/docs/programming_with_holes.org b/docs/programming_with_holes.org new file mode 100644 index 00000000..94f3695b --- /dev/null +++ b/docs/programming_with_holes.org @@ -0,0 +1,39 @@ +:PROPERTIES: +:ID: 69ab6084-2e41-4893-82b8-85ac04b1b1ca +:END: +#+title: Programming with holes + +* Programming with holes + +=Holes=, in programming, are something used to tell the language that +a part of the program is incomplete. Some languages like Idris and +Agda natively support =typed holes=. The way I see it, holes are used +to falicitate the conversation between the programmer and the +compiler. + +But, for languages like common lisp that doesn't support holes +out-of-the box, how could we do that? In general, there are no symbol +name that will never clash with other symbols, because symbols in +common lisp can be any string. One idea is to use inline comments, +like ~#| hole-name |#~. Breeze's parser would be able to recognize +them and manipulate them. + +But what for? + +** Snippets + +Holes can be used to both tell the user what he is expected to enter +in a snippet and tell the editor where the user is expected to enter +stuff. + +** Typing + +A user could use a hole to tell the editor to infer the type of an +expression or function and replace the hole by the appropriate +declaration. + +** Program synthesis + +A user could use a hole to tell the editor to find the right +expression where the hole is. This probably requires that the user +specify some more constraints, by giving types, writing tests, etc. diff --git a/docs/reader-macros.org b/docs/reader-macros.org new file mode 100644 index 00000000..f3655b8a --- /dev/null +++ b/docs/reader-macros.org @@ -0,0 +1,148 @@ +:PROPERTIES: +:ID: 9cb1f9d0-572f-4b8c-bbc8-4c1bb9a54eb4 +:END: +#+title: Reader macros + +https://quickdocs.org/-/search?q=reader + +* Example of a custom reader macro +:PROPERTIES: +:ID: 68e2f3b0-264e-4844-b38f-92be13cca6ea +:END: + +#+begin_src +(with-input-from-string (input "{ hey }") + (let ((*readtable* (copy-readtable))) + (set-macro-character #\{ (lambda (stream char) + (read-delimited-list #\} stream))) + (read input))) +;; => (HEY) +#+end_src + +* cl-annot +:PROPERTIES: +:ID: 11702123-9dc1-4ca4-9325-53d4ac4188cc +:END: + +=@asfd= + +* Eclector's =syntax-extensions= module +:PROPERTIES: +:ID: 3c68d188-b4c9-4ab1-aeb8-cee25aaa8273 +:END: + +From eclector 0.10 release's notes: + +#+begin_src +p::(a b) +=== +(p::a p::b) +#+end_src + +#+begin_src +#; (this form is commented out) +#2; (these 2 forms) (are commented out) +#+end_src + +* Named readtables +:PROPERTIES: +:ID: f5fa06ac-75a3-4dbf-8ed3-17c320ff2927 +:END: + +#+begin_src +(in-readtable ) +(named-readtables:in-readtable ...) +#+end_src + +* CommonQt/Qtools +:PROPERTIES: +:ID: 16bbdda7-ce07-456b-be44-fd787c712c5f +:END: + +#+begin_src +#_ +q+ +#> #< +#' +#+end_src + +P.S. These are not maintained anymore + +* Library "Reader" +:PROPERTIES: +:ID: 0cddf3d0-b37a-4a66-83dd-05d1e63dea33 +:END: + +- https://quickdocs.org/reader + +#+begin_src +(reader:enable-reader-syntax ...) + +#[ +{eq +{eql +{equal +#{ +#! + +! (not ) +$ "ensure-string" +#+end_src + +* Shebang +:PROPERTIES: +:ID: bc2db964-8402-42e6-8992-dc754941f8c4 +:END: + +at least sbcl and roswell + +#+begin_src +#! +#+end_src + +* cl-interpol +:PROPERTIES: +:ID: 1188ce38-45c1-426d-aab1-b4d209baef62 +:END: + +#+begin_src +(named-readtables:in-readtable :interpol-syntax) +or +(cl-interpol:enable-interpol-syntax) +(cl-interpol:disable-interpol-syntax) +#+end_src + +#+begin_quote +The question mark may optionally be followed by an R and an X (case +doesn't matter) - see the section about regular expression syntax +below. If both of them are present, the R must precede the X. +#+end_quote + +#+begin_quote +The next character is the opening outer delimiter which may be one of + - ="= (double quote), + - ='= (apostrophe), + - =|= (vertical bar), + - =#= (sharpsign), + - =/= (slash), + - =(= (left parenthesis), + - =<= (less than), + - =[= (left square bracket), or + - ={= (left curly bracket). +(But see =*OUTER-DELIMITERS*=.) +#+end_quote + +* curry-compose-reader-macros +:PROPERTIES: +:ID: eff9b0b6-ceb3-4882-bdec-1ab212fb20fc +:END: + +https://quickdocs.org/curry-compose-reader-macros + +#+begin_src +(in-readtable :curry-compose-reader-macros) +{+ 1} +[#'list {* 2}] +«list {* 2} {* 3}» +‹if #'evenp #'1+ #'1-› +#+end_src diff --git a/docs/roadmap.org b/docs/roadmap.org new file mode 100644 index 00000000..d08b3461 --- /dev/null +++ b/docs/roadmap.org @@ -0,0 +1,21 @@ +:PROPERTIES: +:ID: 11dd9906-75ff-4abc-82a5-b7dda0936f06 +:END: +#+title: Roadmap + +* Roadmap + +This roadmap serves as a rule of thumb for priorisation. + +1. Improve the reader, add tests +2. Make commands more declarative (see [[file:src/pattern.lisp][pattern.lisp]]) +3. Improve the documentation +4. Implement better refactor commands +5. Improve breeze-eval function, add tests +6. Reduce breeze's dependencies +7. Add support for other editors + +** Status of the roadmap + +As of 2023-11-24, all the points from 1 through 5 are being worked on +concurrently. diff --git a/docs/silimar_projects.org b/docs/silimar_projects.org new file mode 100644 index 00000000..8a706769 --- /dev/null +++ b/docs/silimar_projects.org @@ -0,0 +1,9 @@ +:PROPERTIES: +:ID: 62112623-6002-4cb9-87de-cb530ce0a36e +:END: +#+title: Silimar projects + +* Python + +- [[id:5eb1faac-b7b5-4c99-abe9-b91e77bea4ae][Jedi]] +- [[id:5265bce5-c6d0-4cda-8d3e-699ceafcab42][Elpy]] diff --git a/docs/sly_slime_integration.org b/docs/sly_slime_integration.org new file mode 100644 index 00000000..52b9a4bd --- /dev/null +++ b/docs/sly_slime_integration.org @@ -0,0 +1,44 @@ +:PROPERTIES: +:ID: 54e6cd55-803b-4e15-82bc-a332130d020e +:END: +#+title: Sly/Slime integration + +* Other projects with slime/sly integration + +** log4cl + +- https://github.com/sharplispers/log4cl/blob/master/log4cl.log4slime.asd +- https://github.com/sharplispers/log4cl/blob/master/log4cl.log4sly.asd +- https://github.com/sharplispers/log4cl/tree/master/elisp + +** cepl + +- https://github.com/cbaggers/cepl/blob/master/docs/single-thread-swank.md +- https://github.com/cbaggers/livesupport +- https://github.com/cbaggers/swank.live + +** cl-routes + +https://github.com/archimag/cl-routes/blob/master/src/routes-swank.lisp + +* TODO My old elisp snippet to eval with slime and kill the result + +https://gist.github.com/fstamour/2d7569beaf42c0a0883dc0ae559c6638 + +#+begin_src emacs-lisp +(defun slime-eval-save-output (string) + "Evaluate STRING in Lisp and save the result in the kill ring." + (slime-eval-async `(swank:eval-and-grab-output ,string) + (lambda (result) + (cl-destructuring-bind (output value) result + (kill-new output) + (message "Evaluation finished; pushed output to kill ring."))))) + + +(defun lisp-eval-defun-in-kill-ring () + (interactive) + (slime-eval-save-output (slime-defun-at-point))) + +(global-set-key (kbd "C-M-z") 'lisp-eval-defun-in-kill-ring) +#+end_src +* TODO https://github.com/melisgl/mgl-pax for more emacs/slime integration :editor: diff --git a/docs/style.css b/docs/style.css index a0f33f26..14de6cd4 100644 --- a/docs/style.css +++ b/docs/style.css @@ -6,6 +6,15 @@ Table of Contents: 3. Classes */ + +:root { + --text-color: black; + --dimmed-text-color: #777; + /* TODO better name? */ + --contrast-background: #f4f5f6; +} + + /* 1. Base Tags –––––––––––––––––––––––––––––––––––––––––––– */ @@ -21,7 +30,7 @@ ul, ol, dl, fieldset, pre, pre > code { } body { margin: auto; - max-width: 50rem; + max-width: 80rem; padding: 2rem 0.5rem 0rem; overflow-x: hidden; } @@ -29,7 +38,11 @@ footer { margin: 10rem 0rem 0rem; } /* text */ -:root { font: 13pt 'Open Sans', sans-serif; line-height: 1.5; color: #424456; } +:root { + font: 13pt 'Open Sans', sans-serif; + line-height: 1.5; + color: var(--text-color) +} small, sub, sup { font-size: 75%; } @@ -42,9 +55,13 @@ h1 { font-size: 2.5em; font-weight: 300; } h2 { font-size: 2.0em; font-weight: 300; } h3 { font-size: 1.5em; font-weight: 400; } h4 { font-size: 1.1em; font-weight: 700; margin: .5em 0 0em; } -h5 { font-size: 1.2em; font-weight: 400; margin-top: 0.5em; color: #777; } +h5 { font-size: 1.2em; font-weight: 400; margin-top: 0.5em; color: var(--dimmed-text-color); } h6 { font-size: 1.0em; font-weight: 700; } -p strong { color: #424456; font-weight: bold; font-size: 1.0em; } +p strong { + color: var(--text-color); + font-weight: bold; + font-size: 1.0em; +} /* lists */ @@ -59,14 +76,14 @@ td, th { } td:first-child, th:first-child { padding-left: 0; text-align: left; } td:last-child, th:last-child { padding-right: 0; } -tr:hover{ background-color: #f4f5f6; } +tr:hover{ background-color: var(--contrast-background); } /* figures */ img { max-width: 100%; } figure { text-align: center; } figure > img { display: block; margin: 0.5em auto; } -figcaption, caption { color: #777; margin-bottom: 1rem; } +figcaption, caption { color: var(--dimmed-text-color); margin-bottom: 1rem; } /*code*/ @@ -74,10 +91,11 @@ pre > code { margin: 0; padding: 0.5rem 1.0rem; border-left: 0.3rem solid #3273dc; - background-color: #f4f5f6; + background-color: var(--contrast-background); + width: 80em; } code, kbd, samp { - padding: 0.3em; background: #f4f5f6; white-space: pre; font-size: 90%; + padding: 0.3em; background: var(--contrast-background); white-space: pre; font-size: 90%; } @@ -91,7 +109,7 @@ a:hover, button:not([disabled]):hover { /* forms and inputs */ textarea, input, button, select { border-radius: .3rem; border: .1rem solid #d1d1d1; padding: 0.3rem; outline: none;} -button { padding: 0.7rem 1rem; cursor: pointer; font-weight: bold; letter-spacing: 0.1rem; color: #0065bd; background-color: #f4f5f6; border: .1rem solid #0065bd; } +button { padding: 0.7rem 1rem; cursor: pointer; font-weight: bold; letter-spacing: 0.1rem; color: #0065bd; background-color: var(--contrast-background); border: .1rem solid #0065bd; } /* misc */ @@ -123,7 +141,7 @@ main aside { /* Extra: navbar */ body nav { width: 100%; min-height: 3rem; - background-color: #f4f5f6; + background-color: var(--contrast-background); border-bottom: solid 0.1rem #d1d1d1; } body > nav { position: fixed; top: 0; left: 0; } @@ -147,7 +165,7 @@ nav ul > li > ul { width: auto; position: absolute; padding: 1.0rem 2.5rem 0rem; - background-color: #f4f5f6; + background-color: var(--contrast-background); z-index: 2; } nav ul > li > ul > li { white-space: nowrap; } @@ -220,13 +238,9 @@ mark{ background-color: transparent; color: #007c30; text-decoration: underline /* colors */ -.text-black { color: #000; } -.text-white { color: #fff; } .text-primary { color: #0065bd; } .text-secondary{ color: #333; } -.bg-white { background-color: #fff; } -.bg-light { background-color: #f4f5f6; } .bg-primary { background-color: #0065bd; } .bg-secondary{ background-color: #d1d1d1; } @@ -296,3 +310,135 @@ mark{ background-color: transparent; color: #007c30; text-decoration: underline [class*="col"] > * { margin-top: 1rem !important; } } @media (max-width: 61rem) { aside {position: relative; margin: 0.5rem 0; } } + + +code .progn { + color: var(--text-color); + background-color: var(--contrast-background); +} + +code .symbol{ + color: #770055; + background-color: var(--contrast-background); +} + +code a.symbol:link, +code a.symbol:active, +code a.symbol:visited, +code a.symbol:hover{ + color: #2AA198; + background-color: var(--contrast-background); +} + +code .special { + color: #CB4B16; + background-color: var(--contrast-background); +} + +code .keyword { + color: #770000; + background-color: var(--contrast-background); +} + +code .comment { + color: #586E75; + background-color: var(--contrast-background); +} + +code .string { + color: #B58900; + background-color: var(--contrast-background); +} + +code .atom { + color: #314F4F; + background-color: var(--contrast-background); +} + +code .macro { + color: #CB4B16; + background-color: var(--contrast-background); +} + +code .variable { + color: #36648B; + background-color: var(--contrast-background); +} + +code .function { + color: #8B4789; + background-color: var(--contrast-background); +} + +code .attribute { + color: #CB4B16; + background-color: var(--contrast-background); +} + +code .character { + color: #0055AA; + background-color: var(--contrast-background); +} + +code .syntaxerror { + color: #FF0000; + background-color: var(--contrast-background); +} + +code .diff-deleted { + color: #5F2121; + background-color: var(--contrast-background); +} + +code .diff-added { + color: #215F21; + background-color: var(--contrast-background); +} + +code span.paren1, +code span.paren2, +code span.paren3, +code span.paren4, +code span.paren5, +code span.paren6 + { + transition: background-color 0.2s linear, + border-color 0s, + font-size 0.3s ease-out; +} + + +code span.paren1:hover, +code span.paren2:hover, +code span.paren3:hover, +code span.paren4:hover, +code span.paren5:hover, +code span.paren6:hover +{ + transform: scale(1.5); + /* font-size: 110%; */ +} + +code span.paren1:hover { + background-color: #BAFFFF; +} + +code span.paren2:hover{ + background-color: #FFCACA; +} + +code span.paren3:hover{ + background-color: #FFFFBA; +} + +code span.paren4:hover{ + background-color: #CACAFF; +} + +code span.paren5:hover{ + background-color: #CAFFCA; +} + +code span.paren6:hover{ + background-color: #FFBAFF; +} diff --git a/docs/support_for_bug_reports.org b/docs/support_for_bug_reports.org new file mode 100644 index 00000000..11deefb1 --- /dev/null +++ b/docs/support_for_bug_reports.org @@ -0,0 +1,21 @@ +:PROPERTIES: +:ID: 1bfee55a-11ef-47d6-924b-2ce1a9b39f3b +:END: +#+title: Support for bug reports + +* TODO Make it easy to report a bugs :ux:ops: + +- OS version +- lisp version +- editor version +- quicklisp + - client version + - distributions +- for each dependency + - from which distribution the system come from + - version of the system + +* Generalized support for bug reports :idea: + +Maybe there could be a way to report bugs for any projects (not just +breeze)? diff --git a/docs/support_for_tests.org b/docs/support_for_tests.org new file mode 100644 index 00000000..33bd0938 --- /dev/null +++ b/docs/support_for_tests.org @@ -0,0 +1,52 @@ +:PROPERTIES: +:ID: a9a98f8e-b097-4e8c-a2d1-92d8b8a26707 +:END: +#+title: Support for tests + +* PROtocol and TESTcase manager :test:3rd_parties: + +[[https://github.com/phoe/protest][phoe/protest]] + +PROTEST is a tool for defining protocols and test cases written in and +for Common Lisp. +* trying to find discrepancies between the packages and test packages :test: + +or betweew test system and the system under test + +I consider this task "DONE" because I did _try_ to find discrepancies +between the package ~breeze.refactor~ and ~breeze.test.refactor~. I +used the convention that each "command" defined in ~breeze.refator~ +should have a test with the same name (i.e. the same symbol-name). I +have a test that fails if this "invariant" is not held. + +In the future, I would like to + +** TODO Figure out how to generalize "finding missing tests by discrepancies" :test:ux:config: + +Not everyone is going to have the same conventions. + +** TODO Improve the current test by looking for prefix instead + +E.g package ~a~ has an exported symbol ~s~, it's corresponding test +package is ~a.test~. + +The current implementation would try to find a test named ~s~ in +~a.test~ (for example ~a.test::s~, or ~"s"~ (test names can be string +in parachute), it would be nice to have it also considers tests that +have the _prefix_ ~s~. + +Why? Because I have some automatically generated test (a bit like +snapshot tests), it's very convenient that they have the same name as +the thing they are testing. Using a prefix would let me have multiple +kind of tests for each (automatically generated or not). + +Do I want to check if each "types" of tests are implemented? + +Can parachute's `deftest`'s be easily augmented with some metadata? +That might help too. + +* TODO Integrate with multiple test framework :test: + +See @phoe's [[https://github.com/phoe/protest][phoe/protest]]. + +* TODO It's too easy to kill the test-runner :tech_debt:ux: diff --git a/docs/tags.org b/docs/tags.org new file mode 100644 index 00000000..01ec92bc --- /dev/null +++ b/docs/tags.org @@ -0,0 +1,77 @@ +* Tags + +** TODO This is broken since I started to split everything into small files + +Use =org-roam-db-query= + +example: +https://d12frosted.io/posts/2021-01-16-task-management-with-roam-vol5.html + +See https://github.com/ahmed-shariff/org-roam-ql + +** Tag descriptions + +#+NAME: tags +| Tag name | Tag description | +|--------------+-----------------------------------------------------------------------------------------------| +| 3rd_parties | Relating to a third-party, e.g. an external library. | +| obsolete | This task is now obsolete. | +| doc | Relating to the documentation. | +| ux | This task is about improving the user experience | +| test | This task is about testing | +| ops | This task is about CI, releases, deploying docs, etc. | +| easy | This task should be easy | +| bug | This is an unintended bug | +| editor | This task relates to the integration with an editor. | +| config | Relating to breeze's configuration and setup. | +| refactor | Relating to breeze's refactoring facilities. | +| capture | Relating to breeze's capture feature. | +| quickproject | Relating to quickproject integration. | +| tech_debt | Due to an ongoing refactoring, to an old hack, incomplete implementation, missing tests, etc. | +| reader | Relating to breeze.reader. | +| noexport | org-mode internal tag | + + +#+begin_src emacs-lisp :var tags=tags + ;; (prin1-to-string (org-get-buffer-tags)) + ;; (prin1-to-string tags) + + ;; Find tags that have no descriptions + (let ((unknown-tags + (cl-set-difference + (mapcar #'car (org-get-buffer-tags)) + (mapcar #'car tags) + :test #'string=))) + (or unknown-tags + "All good, no tags without description found.")) +#+end_src + +#+RESULTS: +: All good, no tags without description found. + +** Make sure all tasks have some tags + +#+begin_src emacs-lisp + (let ((result)) + (org-map-entries (lambda () + ;; (org-entry-is-todo-p) + (cl-destructuring-bind (level reduced-level todo priority headline _tags) + (org-heading-components) + ;; _tags does not contain the inherited tags + (when (and + todo + ;; todo could be "DONE" for example + (string= todo "TODO") + (not (org-get-tags))) + (push (list headline) result))))) + (nreverse result)) +#+end_src + +#+RESULTS: +| Programming with holes | + + +* Local variables :noexport: +# local variables: +# org-confirm-babel-evaluate: nil +# end: diff --git a/docs/task-generate_web_pages_from_org_files_in_docs.org b/docs/task-generate_web_pages_from_org_files_in_docs.org new file mode 100644 index 00000000..39007a09 --- /dev/null +++ b/docs/task-generate_web_pages_from_org_files_in_docs.org @@ -0,0 +1,6 @@ +:PROPERTIES: +:ID: 02d8e1a2-ecea-4c47-8808-b5f7a906b553 +:END: +#+title: Generate web pages from org files in docs/ + +* TODO Generate web pages from org files in docs/ diff --git a/docs/tasks.org b/docs/tasks.org new file mode 100644 index 00000000..85dca58e --- /dev/null +++ b/docs/tasks.org @@ -0,0 +1,231 @@ +:PROPERTIES: +:ID: a56d3ee1-b97c-4201-b210-c5d20e602663 +:END: +#+title: Tasks +#+todo: TODO | DONE OBSOLETE + +* Tasks + +**Regularly review the tasks** it helps to see the big picture and +prioritize accordingly. + +** TODO Use with-branching to define optimal-string-alignment-distance :tech_debt: + +Instead of having 2 definitions + +https://github.com/phoe/with-branching + +** TODO Basic commands should send the buffer to the editor :editor: + +e.g. If the common-lisp side ask the editor to insert a string, but +the user changed buffer, the editor will insert the string in the +wrong buffer. + +OR the "client" should be responsible to always execute the commands +with on the "initial" buffer, unless stated otherwise (but that would +add even more state on the protocol). + +** DONE Remove the command ~backward-char~ :tech_debt:editor: + +I introduced the command ~backward-char~ as a hacky workaround: the +integration with the editor edits the buffer incrementally, but then +some other editor configuration would interfere with that process (for +example =aggressive-indent-mode= in emacs. + +I would prefer to improve the whole integration with the editor than +using "backward-chard" as a crutch. + +Something like ~relative-move~ would be better. + +** Showcase/demo :doc: + +*** TODO Show the integration with quickproject :doc:quickproject: + +*** TODO Show how breeze can help create a project from a file :doc:refactor:capture: + +e.g. you made a "capture" with breeze, and now you want to turn that +into a project. + +*** TODO Create a demo to show how to install breeze :doc:config: + +** TODO Add command "go to next missing documentation" :editor: + +** TODO Add command "got to next documentation" :editor: + +To help revision + +** TODO Add command "next todo" :editor: + +Don't re-implement it, just figure out which how to configure the +editor to do that. + +** TODO Use header-line in emacs to show the test results :editor:ux: + +Instead of a lighter + +The lighter could still be used though, it could be a ✓ or χ. + +** DONE Better explain what Breeze currently is in the readme :doc: + +As opposed to explain the principles, goals, non-goals, etc. + +Actually explain what breeze *does*. + +** DONE add a "What is this?" section :doc: +** OBSOLETE [#A] On first setup, the user has to (ql:quickload 'breeze) :ux:obsolete: + +Start by documenting how to start using breeze, then automate it. + +N.B. Since I remove the test-runner and etc. breeze doesn't really +need to be "started". But it still needs to be loaded. + +** TODO Add links to all the "TODOs" in the documentation :doc: + +grep TODO -h | to_github_url | to_html + +** DONE Breeze shouldn't suggest symbol that are "too far" :ux: + +e.g not suggest "slot-exists-p" when trying to eval +"this-really-doesnt-exists". + +*** TODO Still need to add tests on this... :test:tech_debt: +** DONE Make a "string distance" function that stops after a threshold + +** TODO Use a heap to find the N closest matches :ux: + +- Maybe look into VP-trees (Vantage-Point trees) + +How would that help the user? Adding a restart for each candidate +would really pollute the list of restarts. Perhaps we could keep the +current restart, and add another one that shows more candidate +(restarts). + +Maybe we can refine that list of candidate based on other, perhaps +heavier criteria? + +** TODO Document (with screenshots) how to setup and use emacs integration :doc: +** TODO Document how to use quickproject integration :doc: +** TODO Add binding to run test at point :ux: +** TODO Add command to choose a test and run it :ux: +** TODO [#B] When inserting a package definition in an empty buffer, evaluate the buffer :easy:ux: +** TODO Generate the documentation in the CI :ops:doc: + +Really not a priority, even though generating the documentation +locally and committing the result is less than ideal, it works well. + +The main problem is that, AFAIK, you can't host something in github +pages without committing it into a repository. Which means that you +have to hack a CI pipeline that commit its results back into the +repository. This sucks IMO. + +An alternative would be to use GitLab pages, which are way more sane +as they allow (require, really) that your pages' content be generated +from the CI pipeline (from a job called "pages" to be exact). On the +other hand (again), I've had really janky load time with GitLab, but +that might just have been the Authentication + boatload of js. + +** TODO [#C] Add a link to the GitHub repository in the documentation :doc:ux: +** Add integration tests +*** TODO Look into emacs-director :test: + +https://github.com/bard/emacs-director +*** TODO Look into makem.sh :test:ops: + +[[https://github.com/alphapapa/makem.sh][makem.sh]] - Makefile-like script for building and testing Emacs Lisp +packages + +** TODO Try to detect when the current buffer/file was not loaded (evaluated). :ux: + +The goal would be to warn the user "hey, you're trying to evaluate +that function, but the package declared in this file/buffer doesn't +exists". + +Could pass the file to br:next. What if +- it's a buffer that's not visiting a file? +- it's not a buffer in lisp-mode +- the buffer is empty +- the buffer requires some reader-macro +- the buffer is visiting a file, but there are unsaved modifications + +** TODO Try to suggest new and old projects? :ux: + +When the user just initialized breeze, try to find out if the user has +any lisp project(s) already opened, help him work on it. + +If not projects are found guide him through =breeze-scaffold= + +** TODO Suggest corrections when typos are detected :ux: + +We already suggest stuff when there's, for example, an +undefined-function error. We could go one step further and suggest a +quickfix in the editor. We should probably suggest that quickfix only +when the edit-distance is not too great, or we would get some wild quickfixes. + +** TODO Maybe add this document (notes.org) to the documentation? :doc: + +** TODO Maybe split the documentation in multiple pages :doc: + +** Commands :editor: + +*** wrap with :refactor: + +**** TODO let + +**** TODO multiple-value-bind + +*** TODO add import-from :refactor: + +already has a prototype in emacs lisp + +*** TODO move-form-into-let :refactor: + +already has a prototype in emacs lisp + +*** TODO Comment current form :refactor: + +*** TODO Move top-level form up/down :refactor: + +A.k.a transpose-forms, but keep the cursor at the start of the form +that we just moved. + +** Follow up on issues :3rd_parties: + +*** In [[https://github.com/slime/slime][Slime]] + +**** TODO [[https://github.com/slime/slime/issues/645][Is there a way to run a function when slime's repl is ready #645]] + +*** In [[https://github.com/phoe-trash/value-semantics-utils][phoe-trash/value-semantics-utils]] + +**** TODO [[https://github.com/phoe-trash/value-semantics-utils/issues/4][Utilities to update a set of object while maximising structural sharing #4]] + +**** TODO [[https://github.com/phoe/trivial-method-combinations/issues/4][Add method-combination-name-p #4]] + +*** In [[https://github.com/phoe/external-symbol-not-found/][phoe/external-symbol-not-found]] + +**** TODO [[https://github.com/phoe/external-symbol-not-found/issues/1][Improve tests #1]] + +**** TODO [[https://github.com/phoe/external-symbol-not-found/issues/2][Add CI pipeline #2]] + +**** TODO [[https://github.com/phoe/external-symbol-not-found/issues/3][Add a macro that transforms the conditions into portable conditions #3]] + +*** In [[https://github.com/40ants/ci/issues/7][40ants/ci]] + +**** TODO [[https://github.com/40ants/ci/issues/7][Question: How to generate jobs to test with multiple implementation? #7]] + +*** In [[https://github.com/s-expressionists/Eclector][s-expressionists/Eclector]] + +**** [[https://github.com/s-expressionists/Eclector/issues/28][Ensure every CST element has a SOURCE, suggest how to capture whitespace and comments #28]] + +** TODO Add code coverage :test:ops: + +** TODO Fake packages? :reader: + +- https://github.com/informatimago/lisp/blob/4bfb6893e7840b748648b749b22078f2facfee0a/common-lisp/lisp-reader/package-pac.lisp +- https://github.com/s-expressionists/Clostrum +* TODO Add command to add package local nicknames + +#+begin_src lisp +(add-package-local-nickname '#:jzon '#:com.inuoe.jzon) +#+end_src + +* TODO command to "copy current package" / "package at point" diff --git a/docs/test.md b/docs/test.md deleted file mode 100644 index d018971d..00000000 --- a/docs/test.md +++ /dev/null @@ -1,13 +0,0 @@ -# Test framework - -## Definition - -- deftest -- is - -`is` can be called by itself, it will execute the test. - -## Aux - -- run-all-tests - diff --git a/docs/testing-breeze.org b/docs/testing-breeze.org new file mode 100644 index 00000000..561e8017 --- /dev/null +++ b/docs/testing-breeze.org @@ -0,0 +1,23 @@ +:PROPERTIES: +:ID: e712f3d1-0734-43f0-886a-3008ca5f722d +:END: +#+title: Testing Breeze + +* How to run the tests + +#+begin_src lisp +(ql:quickload "breeze/test") +(asdf:test-system "breeze") +#+end_src + +Or from the command line: + +#+begin_src shell +./scripts/test.sh +#+end_src + +OR + +#+begin_src shell +make test +#+end_src diff --git a/docs/visual_studio_code_integration.org b/docs/visual_studio_code_integration.org new file mode 100644 index 00000000..740fb134 --- /dev/null +++ b/docs/visual_studio_code_integration.org @@ -0,0 +1,6 @@ +:PROPERTIES: +:ID: 086c7705-e5ec-4dc0-852d-211c055eb145 +:END: +#+title: Visual Studio Code integration + +TODO diff --git a/error-system-loadedp.txt b/error-system-loadedp.txt new file mode 100644 index 00000000..8cdcd21f --- /dev/null +++ b/error-system-loadedp.txt @@ -0,0 +1,23 @@ +There is no applicable method for the generic function + # +when called with arguments + (#P"/home/fstamour/quicklisp/local-projects/sb-unix-socket/sb-unix-socket.asd"). + [Condition of type SB-PCL::NO-APPLICABLE-METHOD-ERROR] +See also: + Common Lisp Hyperspec, 7.6.6 [:section] + +Restarts: + 0: [RETRY] Retry calling the generic function. + 1: [ABORT] abort thread (#) + +Backtrace: + 0: ((:METHOD NO-APPLICABLE-METHOD (T)) # #P"/home/fstamour/quicklisp/local-projects/sb-unix-socket/sb-unix-socket.asd") [fast-method] + 1: (SB-PCL::CALL-NO-APPLICABLE-METHOD # (#P"/home/fstamour/quicklisp/local-projects/sb-unix-socket/sb-unix-socket.asd")) + 2: ((LABELS ASDF/COMPONENT::RECURSE :IN ASDF/COMPONENT:SUB-COMPONENTS) #P"/home/fstamour/quicklisp/local-projects/sb-unix-socket/sb-unix-socket.asd") + 3: (ASDF/COMPONENT:SUB-COMPONENTS #P"/home/fstamour/quicklisp/local-projects/sb-unix-socket/sb-unix-socket.asd" :TYPE T) + 4: (BREEZE.ASDF:LOADEDP "/home/fstamour/quicklisp/local-projects/sb-unix-socket/sb-unix-socket.lisp") + 5: (BREEZE.REFACTOR::MAYBE-ASK-TO-LOAD-SYSTEM) + 6: ((LAMBDA NIL :IN BREEZE.REFACTOR:QUICKFIX)) + 7: (BREEZE.COMMAND::CALL-WITH-COMMAND-SIGNAL-HANDLER #) + 8: (BREEZE.COMMAND::CANCEL-COMMAND-ON-ERROR 2 #) + 9: ((LABELS BORDEAUX-THREADS::%BINDING-DEFAULT-SPECIALS-WRAPPER :IN BORDEAUX-THREADS::BINDING-DEFAULT-SPECIALS)) diff --git a/kite/kite.lisp b/kite/kite.lisp index 3b20870f..54300218 100644 --- a/kite/kite.lisp +++ b/kite/kite.lisp @@ -1,16 +1,18 @@ + +;;; breeze.kite package + (uiop:define-package #:breeze.kite (:documentation "Utilities for the test framework parachute.") (:use #:cl) - (:import-from #:parachute - #:define-test - #:define-test+run - #:true - #:false - #:of-type) - (:export #:is-equalp)) + (:export + #:is + #:is-equalp + #:is-equalp*)) (in-package #:breeze.kite) + +;;; WIP ;; Find empty tests #++ @@ -30,8 +32,35 @@ :when (stringp name) :do (parachute:remove-test test)) - -(defun is-equalp (input got &optional expected description &rest format-args) + +;;; Assertion helpers + +(defun is (&key + comparator + expected + got + form + description format-args) + "The defun equivalent of the parachute:is macro." + (parachute:eval-in-context + parachute:*context* + (make-instance 'parachute:comparison-result + :expression `(is ,comparator ,expected ,form) + :value-form form + :body got + :expected expected + :comparison comparator + :description (when description + (apply 'format nil description format-args))))) + + +(defun is-equalp (&key + input + got + (form nil form-supplied-p) + expected + description + format-args) "Helper for testing that GOT and EXPECTED are EQUALP. Can be run interactively. @@ -57,58 +86,81 @@ that message to *trace-output* and return it as a second value. "~t~t~s~%" "~texpected:~%" "~t~t~s"))) - #++ - (format t "~%control: ~s~%input: ~s~%got: ~s~%expected: ~s~%description: ~s~%format-args: ~s" - control input got expected description format-args) (flet ((fmt (&rest args) (let ((str (apply #'format nil args))) (unless parachute:*context* (format *trace-output* "~&~a" str)) str))) - (parachute:is equalp expected got - control - input (list description format-args) got expected) + (is + :comparator 'equalp + :expected expected + :got got + :form (if form-supplied-p form got) + :description control + :format-args (list input + (list description format-args) got expected)) (if (equalp expected got) - got + (values got :passed) (values got (fmt control input (list description format-args) got expected)))))) + +(defun is-equalp* (input got &optional expected description &rest format-args) + "Helper for testing that GOT and EXPECTED are EQUALP. + +Can be run interactively. + +Can be run without an expected value. + +Will always return GOT. + +If GOT is not equalp to EXPECTED, generate a nice error message. Print +that message to *trace-output* and return it as a second value. +" + (is-equalp + :input input + :got got + :expected expected + :description description + :format-args format-args)) + + #| Examples -(is-equalp "32 " 2) +(is-equalp* "32 " 2) returns 2 prints: For «32 » - got: - 2 - expected: - NIL +got: +2 +expected: +NIL -(is-equalp "32 " 2 1) +(is-equalp* "32 " 2 1) returns 2 prints For «32 » - got: - 2 - expected: - 1 +got: +2 +expected: +1 -(is-equalp "32 " 2 2) +(is-equalp* "32 " 2 2) returns 2 doesn't print -(is-equalp "32 " 2 1 " (~{~a~^, ~})" '(a b c)) +(is-equalp* "32 " 2 1 " (~{~a~^, ~})" '(a b c)) returns 2 prints For «32 » (A, B, C) - got: - 2 - expected: - 1 +got: +2 +expected: +1 -;; (is-equalp "32 " 2 1 " (~a ~s)" "thirty-two" 32) +;; (is-equalp* "32 " 2 1 " (~a ~s)" "thirty-two" 32) |# diff --git a/makefile b/makefile index 0b831a49..f8d107a1 100644 --- a/makefile +++ b/makefile @@ -1,31 +1,44 @@ # Run the unit tests +.PHONY: test test: scripts/test.sh # Generate the documentation +.PHONY: doc doc: scripts/doc.sh +DOCKER_BUILD := DOCKER_BUILDKIT=1 docker build --progress=plain + +.PHONY: build-within-container +build-within-container: + $(DOCKER_BUILD) --target=$(TARGET) --output type=local,dest=$(or $(DEST),.) . 2>&1 | tee $(TARGET).log + +# Generate the documentation. in a docker +dependencies.core: Dockerfile breeze.asd scripts/load-dependencies.lisp + $(MAKE) build-within-container TARGET=dependencies.core + +.PHONY: integration +integration: dependencies.core + $(MAKE) build-within-container TARGET=integration-tests DEST=public + +.PHONY: public +public: dependencies.core + $(MAKE) build-within-container TARGET=public DEST=public + + # Run some "integration tests" that generates some screenshots # This is work-in-progress +.PHONY: demo demo: scripts/demo/build-docker-image.sh -demo-debug: - scripts/demo/build-docker-image.sh --target debug -t breeze-demo:dev - docker run -it --rm --name breeze-demo breeze-demo:dev bash - # Fix spelling +.PHONY: spell spell: codespell --write-changes --interactive 3 --ignore-words scripts/ignore-words.txt $$(fd -e lisp) README.md notes.org docs/*.md src/breeze.el +.PHONY: watch watch: ( fd . -e lisp src/ tests/; echo breeze.asd ) | entr time scripts/test.sh - -.PHONY: \ - test \ - doc \ - demo \ - spell \ - watch diff --git a/notes.org b/notes.org deleted file mode 100644 index 7e84ad3c..00000000 --- a/notes.org +++ /dev/null @@ -1,950 +0,0 @@ -#+title: Notes -#+todo: TODO | DONE OBSOLETE - -* Roadmap - -1. Improve the reader, add tests -2. Make commands more declarative (see [[file:src/pattern.lisp][pattern.lisp]]) -3. Improve the documentation -4. Implement better refactor commands -5. Improve breeze-eval function, add tests -6. Reduce breeze's dependencies -7. Add support for other editors - -* Tasks - -**Regularly review the tasks** it helps to see the big picture and -prioritize accordingly. - -** DONE fix integration with sly - -I've been using slime only for a long time... Today I tried sly and it -crashed when I tried to use quickfix. - -Here's the problem: - -#+begin_src text -(EVAL (SLYNK:EVAL-AND-GRAB-OUTPUT "(breeze.command:continue-command 1 (1 bit, #x1, #o1, #b1))")) -#+end_src - -Which comes from =breeze.el='s =breeze-interactive-eval= function, -which calls =slynk:interactive-eval=. - -With slime: - -#+begin_src emacs-lisp - (breeze-interactive-eval "1") - ;; => "1" -#+end_src - -With sly: - -#+begin_src emacs-lisp - (breeze-interactive-eval "1") - ;; => "1 (1 bit, #x1, #o1, #b1)" -#+end_src - -** TODO Make it easy to report a bugs :ux:ops: - -- OS version -- lisp version -- editor version -- quicklisp - - client version - - distributions -- for each dependency - - from which distribution the system come from - - version of the system - -** TODO Make sure the commands are executed against _common lisp_ code :ux:editor: - -Gavinok tried breeze, but ran ~C-.~ on ~breeze.el~, this causes -confusion for everybody. - -Here's some ideas to help with this: - -- check the extension of the buffer or file -- check the mode of the buffer -- make sure it's not in the repl (for now) - -** TODO Use with-branching to define optimal-string-alignment-distance :tech_debt: - -Instead of having 2 definitions - -https://github.com/phoe/with-branching - -** DONE Use messages in quickfix :ux:editor: - -e.g. "There is no applicable quickfixes in the current context" - -** TODO docstrings are not kept in (unparse (parse ...)) :bug: - -** TODO Basic commands should send the buffer to the editor :editor: - -e.g. If the common-lisp side ask the editor to insert a string, but -the user changed buffer, the editor will insert the string in the -wrong buffer. - -OR the "client" should be responsible to always execute the commands -with on the "initial" buffer, unless stated otherwise (but that would -add even more state on the protocol). - -** TODO Remove the command ~backward-char~ :tech_debt:editor: - -I introduced the command ~backward-char~ as a hacky workaround: the -integration with the editor edits the buffer incrementally, but then -some other editor configuration would interfere with that process (for -example =aggressive-indent-mode= in emacs. - -I would prefer to improve the whole integration with the editor than -using "backward-chard" as a crutch. - -Something like ~relative-move~ would be better. - -** Showcase/demo :doc: - -*** TODO Show the integration with quickproject :doc:quickproject: - -*** TODO Show how breeze can help create a project from a file :doc:refactor:capture: - -e.g. you made a "capture" with breeze, and now you want to turn that -into a project. - -*** TODO Create a demo to show how to install breeze :doc:config: - -** TODO Add command "go to next missing documentation" :editor: - -** TODO Add command "got to next documentation" :editor: - -To help revision - -** TODO Add command "next todo" :editor: - -Don't re-implement it, just figure out which how to configure the -editor to do that. - -** TODO Use header-line in emacs to show the test results :editor:ux: - -Instead of a lighter - -The lighter could still be used though, it could be a ✓ or χ. - -** DONE Better explain what Breeze currently is in the readme :doc: - -As opposed to explain the principles, goals, non-goals, etc. - -Actually explain what breeze *does*. - -** DONE add a "What is this?" section :doc: -** OBSOLETE [#A] On first setup, the user has to (ql:quickload 'breeze) :ux:obsolete: - -Start by documenting how to start using breeze, then automate it. - -N.B. Since I remove the test-runner and etc. breeze doesn't really -need to be "started". But it still needs to be loaded. - -** TODO Add links to all the "TODOs" in the documentation :doc: - -grep TODO -h | to_github_url | to_html - -** TODO It's too easy to kill the test-runner :tech_debt:ux: - -** DONE Breeze shouldn't suggest symbol that are "too far" :ux: - -e.g not suggest "slot-exists-p" when trying to eval -"this-really-doesnt-exists". - -*** TODO Still need to add tests on this... :test:tech_debt: -** DONE Make a "string distance" function that stops after a threshold - -** TODO Use a heap to find the N closest matches :ux: - -- Maybe look into VP-trees (Vantage-Point trees) - -How would that help the user? Adding a restart for each candidate -would really pollute the list of restarts. Perhaps we could keep the -current restart, and add another one that shows more candidate -(restarts). - -Maybe we can refine that list of candidate based on other, perhaps -heavier criteria? - -** OBSOLETE Document how to use breeze:defun :doc: - -*** When redefining a function defined with breeze:defun, it run tests -*** When defining a test with breeze:deftest, it run tests - -*** Demonstrate that the test-runner is debounced - -When you redefine many function (e.g. when reloading a file), it -doesn't run all the tests each time a something is redefined. - -** TODO Document (with screenshots) how to setup and use emacs integration :doc: -** TODO Document how to use quickproject integration :doc: -** TODO Add binding to run test at point :ux: -** TODO Add command to choose a test and run it :ux: -** TODO [#B] When inserting a package definition in an empty buffer, evaluate the buffer :easy:ux: -** TODO Generate the documentation in the CI :ops:doc: - -Really not a priority, even though generating the documentation -locally and committing the result is less than ideal, it works well. - -The main problem is that, AFAIK, you can't host something in github -pages without committing it into a repository. Which means that you -have to hack a CI pipeline that commit its results back into the -repository. This sucks IMO. - -An alternative would be to use GitLab pages, which are way more sane -as they allow (require, really) that your pages' content be generated -from the CI pipeline (from a job called "pages" to be exact). On the -other hand (again), I've had really janky load time with GitLab, but -that might just have been the Authentication + boatload of js. - -** TODO [#C] Add a link to the GitHub repository in the documentation :doc:ux: -** Add integration tests -*** TODO Look into emacs-director :test: - -https://github.com/bard/emacs-director -*** TODO Look into makem.sh :test:ops: - -[[https://github.com/alphapapa/makem.sh][makem.sh]] - Makefile-like script for building and testing Emacs Lisp -packages - -** TODO Try to detect when the current buffer/file was not loaded (evaluated). :ux: - -The goal would be to warn the user "hey, you're trying to evaluate -that function, but the package declared in this file/buffer doesn't -exists". - -Could pass the file to br:next. What if -- it's a buffer that's not visiting a file? -- it's not a buffer in lisp-mode -- the buffer is empty -- the buffer requires some reader-macro -- the buffer is visiting a file, but there are unsaved modifications - -** TODO Try to suggest new and old projects? :ux: - -When the user just initialized breeze, try to find out if the user has -any lisp project(s) already opened, help him work on it. - -If not projects are found guide him through =breeze-scaffold= - -** TODO Suggest corrections when typos are detected :ux: - -We already suggest stuff when there's, for example, an -undefined-function error. We could go one step further and suggest a -quickfix in the editor. We should probably suggest that quickfix only -when the edit-distance is not too great, or we would get some wild quickfixes. - -** TODO Maybe add this document (notes.org) to the documentation? :doc: - -** TODO Maybe split the documentation in multiple pages :doc: - -** Commands :editor: - -*** wrap with :refactor: - -**** TODO let - -**** TODO multiple-value-bind - -*** TODO add import-from :refactor: - -already has a prototype in emacs lisp - -*** TODO move-form-into-let :refactor: - -already has a prototype in emacs lisp - -*** TODO Comment current form :refactor: - -*** TODO Move top-level form up/down :refactor: - -A.k.a transpose-forms, but keep the cursor at the start of the form -that we just moved. - -** trying to find discrepancies between the packages and test packages :test: - -or betweew test system and the system under test - -I consider this task "DONE" because I did _try_ to find discrepancies -between the package ~breeze.refactor~ and ~breeze.test.refactor~. I -used the convention that each "command" defined in ~breeze.refator~ -should have a test with the same name (i.e. the same symbol-name). I -have a test that fails if this "invariant" is not held. - -In the future, I would like to - -*** TODO Figure out how to generalize "finding missing tests by discrepancies" :test:ux:config: - -Not everyone is going to have the same conventions. - -*** TODO Improve the current test by looking for prefix instead - -E.g package ~a~ has an exported symbol ~s~, it's corresponding test -package is ~a.test~. - -The current implementation would try to find a test named ~s~ in -~a.test~ (for example ~a.test::s~, or ~"s"~ (test names can be string -in parachute), it would be nice to have it also considers tests that -have the _prefix_ ~s~. - -Why? Because I have some automatically generated test (a bit like -snapshot tests), it's very convenient that they have the same name as -the thing they are testing. Using a prefix would let me have multiple -kind of tests for each (automatically generated or not). - -Do I want to check if each "types" of tests are implemented? - -Can parachute's `deftest`'s be easily augmented with some metadata? -That might help too. - -** TODO Integrate with multiple test framework :test: - -See @phoe's [[https://github.com/phoe/protest][phoe/protest]]. - -** Follow up on issues :3rd_parties: - -*** In [[https://github.com/slime/slime][Slime]] - -**** TODO [[https://github.com/slime/slime/issues/645][Is there a way to run a function when slime's repl is ready #645]] - -*** In [[https://github.com/phoe-trash/value-semantics-utils][phoe-trash/value-semantics-utils]] - -**** TODO [[https://github.com/phoe-trash/value-semantics-utils/issues/4][Utilities to update a set of object while maximising structural sharing #4]] - -**** TODO [[https://github.com/phoe/trivial-method-combinations/issues/4][Add method-combination-name-p #4]] - -*** In [[https://github.com/phoe/external-symbol-not-found/][phoe/external-symbol-not-found]] - -**** TODO [[https://github.com/phoe/external-symbol-not-found/issues/1][Improve tests #1]] - -**** TODO [[https://github.com/phoe/external-symbol-not-found/issues/2][Add CI pipeline #2]] - -**** TODO [[https://github.com/phoe/external-symbol-not-found/issues/3][Add a macro that transforms the conditions into portable conditions #3]] - -*** In [[https://github.com/40ants/ci/issues/7][40ants/ci]] - -**** TODO [[https://github.com/40ants/ci/issues/7][Question: How to generate jobs to test with multiple implementation? #7]] - -*** In [[https://github.com/s-expressionists/Eclector][s-expressionists/Eclector]] - -**** [[https://github.com/s-expressionists/Eclector/issues/28][Ensure every CST element has a SOURCE, suggest how to capture whitespace and comments #28]] - -** TODO Add code coverage :test:ops: - -** TODO Fake packages? :reader: - -https://github.com/informatimago/lisp/blob/4bfb6893e7840b748648b749b22078f2facfee0a/common-lisp/lisp-reader/package-pac.lisp - -** TODO Programming with holes - -> I was sure I already had a note about these... - -=Holes=, in programming, are something used to tell the language that -a part of the program is incomplete. Some languages like Idris and -Agda natively support =typed holes=. The way I see it, holes are used -to falicitate the conversation between the programmer and the -compiler. - -But, for languages like common lisp that doesn't support holes -out-of-the box, how could we do that? In general, there are no symbol -name that will never clash with other symbols, because symbols in -common lisp can be any string. One idea is to use inline comments, -like ~#| hole-name |#~. Breeze's parser would be able to recognize -them and manipulate them. - -But what for? - -*** Snippets - -Holes can be used to both tell the user what he is expected to enter -in a snippet and tell the editor where the user is expected to enter -stuff. - -*** Typing - -A user could use a hole to tell the editor to infer the type of an -expression or function and replace the hole by the appropriate -declaration. - -*** Program synthesis - -A user could use a hole to tell the editor to find the right -expression where the hole is. This probably requires that the user -specify some more constraints, by giving types, writing tests, etc. - -* Tags - -** Tag descriptions - -#+NAME: tags -| Tag name | Tag description | -|--------------+-----------------------------------------------------------------------------------------------| -| 3rd_parties | Relating to a third-party, e.g. an external library. | -| obsolete | This task is now obsolete. | -| doc | Relating to the documentation. | -| ux | This task is about improving the user experience | -| test | This task is about testing | -| ops | This task is about CI, releases, deploying docs, etc. | -| easy | This task should be easy | -| bug | This is an unintended bug | -| editor | This task relates to the integration with an editor. | -| config | Relating to breeze's configuration and setup. | -| refactor | Relating to breeze's refactoring facilities. | -| capture | Relating to breeze's capture feature. | -| quickproject | Relating to quickproject integration. | -| tech_debt | Due to an ongoing refactoring, to an old hack, incomplete implementation, missing tests, etc. | -| reader | Relating to breeze.reader. | -| noexport | org-mode internal tag | - - -#+begin_src emacs-lisp :var tags=tags - ;; (prin1-to-string (org-get-buffer-tags)) - ;; (prin1-to-string tags) - - ;; Find tags that have no descriptions - (let ((unknown-tags - (cl-set-difference - (mapcar #'car (org-get-buffer-tags)) - (mapcar #'car tags) - :test #'string=))) - (or unknown-tags - "All good, no tags without description found.")) -#+end_src - -#+RESULTS: -: All good, no tags without description found. - -** Make sure all tasks have some tags - -#+begin_src emacs-lisp - (let ((result)) - (org-map-entries (lambda () - ;; (org-entry-is-todo-p) - (cl-destructuring-bind (level reduced-level todo priority headline _tags) - (org-heading-components) - ;; _tags does not contain the inherited tags - (when (and - todo - ;; todo could be "DONE" for example - (string= todo "TODO") - (not (org-get-tags))) - (push (list headline) result))))) - (nreverse result)) -#+end_src - -#+RESULTS: -| Programming with holes | - -* Design decisions - -** Write everything in common lisp - -As much as possible, so that breeze can easily be ported to different -platforms and editors. - -** Wrap definitions :obsolete: - -Decision: Create wrapper macros (e.g. =br:defun=) to keep the original -forms for later analysis. - -This decision is really not definitive. - -This decision is less than ideal, especially for existing systems, but -it was the easiest to start with. - -*** Alternatives - -**** Keep the string being eval'd - -Advising swank's eval function is "a good start" in that direction. - -**** Parse the source code - -- Might be hard, but [[https://github.com/s-expressionists/Eclector][eclector]] could make this easy. -- [[https://github.com/hyotang666/read-as-string][hyotang666/read-as-string]] is another candidate - -** Migrate to parachute 2022-03-08 - -The test framework and the "wrap definition" parts always were -proof-of-concepts: I wanted to be able to define some tests, and run -them when either the test of the system-under-test was redefined. It -worked, but now that I have a more and more complete common lisp -parser, I can do the things properly. So I've move the concerned code -into the folder "scratch-files" and I'll re-introduce them slowly in -the future. (Because I really want something to run the tests in the -background, for example.) - -** Read from strings instead of streams - -I did some tests and the code was like 100x faster when reading from -string instead of reading from streams. There are multiple reasons: to - extract the "raw" text from the stream require consing new strings -_and_ abusing file-position to move back and forth in the stream, both -of these are very inefficient. Instead, we use displaced arrays which -results in way less consing and no "stream state" to manage. This made -both the code faster and simpler. - -From another point of view: why not? we were already copying the whole -stream into the resulting tree, now we just have references to one -string. - -** Use =licence= and not =license= - -This is a very tiny decision, but I know I'll forget it. - -What made me decide between the two: =licence= is what asdf use, and -it's what the user will see in their project. - -** Only use dependencies from quicklisp's distribution - -This project is not in quicklisp, and I don't plan to add it to -quicklisp until it stabilize (which might take years). But I make sure -that I only use dependencies from quicklisp so that if somebody wants -to try it out they'll just need to clone this repository in -quicklisp's local-projects folder. - -* Other projects with slime/sly integration - -** log4cl - -- https://github.com/sharplispers/log4cl/blob/master/log4cl.log4slime.asd -- https://github.com/sharplispers/log4cl/blob/master/log4cl.log4sly.asd -- https://github.com/sharplispers/log4cl/tree/master/elisp - -** cepl - -- https://github.com/cbaggers/cepl/blob/master/docs/single-thread-swank.md -- https://github.com/cbaggers/livesupport -- https://github.com/cbaggers/swank.live - -** cl-routes - -https://github.com/archimag/cl-routes/blob/master/src/routes-swank.lisp - -* Portable file watching - -https://www.reddit.com/r/lisp/comments/1iatcd/fswatcher_watches_filesystem_changes/ - -http://eradman.com/entrproject/ - -https://github.com/Ralt/fs-watcher (polls) - -https://github.com/Shinmera/file-notify <=== - -2023-09-25 I briefly talked with Shinmera this summer, and they -mentioned that this project doesn't currently work. - -* Random ideas -** (tips), (tips "test"), (tips "doc") -** (next) ;; what's next? print functions that aren't done, that have no tests or documentation. -*** functions that aren't implemented or done -*** functions that have no tests -*** functions that have no documentation -*** Have a plain user-controlled task list -** Evaluate quality of documentation -*** e.g. if the documentation is almost just the name of the function -*** Make sure it doesn't "only" refer to another function -*** It's more that the content of the function - -(defun print-x (x) - "print (* x x)" - (print (* x x)) - -*** Make sure that all package have a :documentation -*** Make sure that all classes have a :documentation -** Evaluation the quality of the code -*** Cyclomatic complexity -*** Length of variable names -*** linting in general -** Compare the files in a system's directory and the actual components. -** See BIST to probalistically compare functions -*** Use a PRNG to generate inputs, use a hash to fingerprint the outputs -See [[file:scratch-files/function-fingerprinting.lisp][function-fingerprinting.lisp]] - -** Generate test for existing functions - -- The more we know the types of the expression, the more we can narrow - down the search. -- It would be easier if we knew which expression are safe to execute - -** Generate code based on desired input/output - -https://github.com/webyrd/Barliman - -- The more we know the types of the expression, the more we can narrow - down the search. -- It would be easier if we knew which expression are safe to execute -- The linter can help choose better results -- Using e-graph to refactor candidates can help suggest helper - functions - - -*** See Programming by examples (inductive synthesis) - -** A lot of things could be done by instrumenting the code - -Which is one of the reason behind wrapping the definitions (e.g. =breeze:defun=) - -- fault injection -- program (dynamic) slicing -- Stepping though code -- profiling -- test coverage -- coverage guided -- profile-guided optimization - -** Program slicing - -*** For code navigation - -It would be nice to be able to search for something (e.g. calls to -make-instance) only in a certain slice (e.g. from the "call tree" of -foo). - -*** Correlate with unit tests - -If we have multiple tests on the same piece of code, we can use the -slices from the tests that pass and the tests that fail to narrow down -which slice is probably the source of the failure. - -** Use equivalence-graph e-graph to suggest refactors - -Main resource: [[https://egraphs-good.github.io/][E-Graphs Good]] - -This might be hard and complicated, I was thinking that I should start -by making this work on a very small scope. For example, if the user -ask to suggest some refactors, we can look for forms that contains -only arithmetic (again, just an example) and nothing else, that use -equality saturation to find interesting equivalent forms and propose -them to the user. - -*** Small discussion I had on lobste.rs about e-graphs on lisp - -- [[https://lobste.rs/s/myyznl/tooling_for_tooling#c_apjopu][Comment on Lobste.rs]] - -The important bit: - -#+begin_quote -egg is great for algebraic rewrites, but doesn’t have good builtin -tools for associative/commutative operations, nor for -alpha-equivalence and rewriting under binders. I deliberately left -names out of my syntax so that rewriting would be easier; this won’t -be as simple for Lisps in general. -#+end_quote - -*** I already started working on implementing equivalence graphs - -A while ago I started by writing a disjoint sets data structure (also -known as union-find, based on the 2 mains operations it supports). - -https://github.com/fstamour/disjoint-sets - -** Semantic diffs using breeze.reader - -* See -** DONE uses locative: http://quickdocs.org/mgl-pax/ :editor: - -2022-03-17 - I read most of the readme, this system looks awesome - -It's mostly for documentation, but it also expand slime/swank for -easier navigation (using the concept of locative). - -** DONE CCL's Watches https://ccl.clozure.com/manual/chapter4.12.html#watched-objects :editor: - -> Clozure CL provides a way for lisp objects to be watched so that a -condition will be signaled when a thread attempts to write to the -watched object - -Very useful for debugging. - -** DONE CCL's Advise https://ccl.clozure.com/manual/chapter4.3.html#Advising - -> The advise macro can be thought of as a more general version of -trace. - -I think I kept this link just for the general interface (~advise~, -~unadvise~ and ~advisep~) - -** TODO https://github.com/melisgl/mgl-pax for more emacs/slime integration :editor: -** TODO SLIMA for integration with Atom :editor: - -Superior Lisp Interactive Mode for Atom - -https://github.com/neil-lindquist/SLIMA - -** TODO An implementation of the Language Server Protocol for Common Lisp :editor: - -- https://github.com/cxxxr/cl-lsp -- related: https://marketplace.visualstudio.com/items?itemName=ailisp.commonlisp-vscode - -** About e-graph - -- https://egraphs-good.github.io/ -- https://colab.research.google.com/drive/1tNOQijJqe5tw-Pk9iqd6HHb2abC5aRid?usp=sharing -- https://arxiv.org/pdf/2004.03082.pdf - -** TODO My old elisp snippet to eval with slime and kill the result - -https://gist.github.com/fstamour/2d7569beaf42c0a0883dc0ae559c6638 - -* Libraries we might need in the future - -** PROtocol and TESTcase manager :test: - -[[https://github.com/phoe/protest][phoe/protest]] - -PROTEST is a tool for defining protocols and test cases written in and -for Common Lisp. - -** Concrete Syntax Tree - -https://github.com/s-expressionists/Concrete-Syntax-Tree -This library is intended to solve the problem of source tracking for -Common Lisp code. - -** SICL - -A fresh implementation of Common Lisp -https://github.com/robert-strandh/SICL - -I'm sure there are tons of other user-case: -- infer types -- interpret code (symbolically or not) - -** How froute uses mop to keep track of a set of definitions - -#+begin_comment -Maybe I should have a section about "code snippets that could be useful"? -#+end_comment - -[[https://github.com/StephenWakely/froute/blob/3d9ea3114537e1451cccec91f7cbe2321a49a1e0/src/froute-class.lisp][froute-class.lisp]] - -* Prior Arts - -** Tinker (1980) -http://web.media.mit.edu/%7Elieber/Lieberary/Tinker/Tinker/Tinker.html - -** Image Based development - -[Image based development](https://www.informatimago.com/develop/lisp/com/informatimago/small-cl-pgms/ibcl/index.html) - -** Code refactoring tools and libraries, linters, etc. - -*** General - -https://comby.dev/ (and https://github.com/s-kostyaev/comby.el) -https://github.com/reviewdog/reviewdog - -*** common lisp - -https://github.com/hyotang666/trivial-formatter -https://github.com/yitzchak/cl-indentify -https://github.com/vindarel/colisper (uses comby) - - its catalog of rewrites: https://github.com/vindarel/colisper/tree/master/src/catalog/lisp -https://github.com/cxxxr/sblint -https://github.com/g000001/lisp-critic/ -https://github.com/eschulte/lisp-format - -*** javascript and front-end in general - -https://github.com/facebookarchive/codemod replaced by -https://github.com/facebook/jscodeshift, which uses -https://github.com/benjamn/recast - -Examples: https://github.com/cpojer/js-codemod - -*** Ruby - -https://github.com/whitequark/parser -https://github.com/seattlerb/ruby_parser -https://github.com/seattlerb/ruby2ruby/ -https://docs.rubocop.org/rubocop-ast/node_pattern_compiler.html -https://nodepattern.herokuapp.com/ -https://github.com/mbj/unparser - -*** Other - -Probably Rosely for C# and clang for C/C++. I'm sure there are tons of -tools/libraries for Java. - -For python, there's the ast module, but I don't know if it can -preserve the formatting. There's a bunch of tools to format the code. - -** Zulu.inuoe's attempt - clution - -- https://github.com/Zulu-Inuoe/clution -- https://github.com/Zulu-Inuoe/clution.lib -- https://github.com/Zulu-Inuoe/lob - -* Breeze on the internets - -** Lisp project of the day - -https://40ants.com/lisp-project-of-the-day/2020/08/0166-breeze.html - -** Reddit - -https://old.reddit.com/r/Common_Lisp/comments/pgtfm3/looking_for_feedbackhelp_on_a_project/ - -*** [[https://old.reddit.com/user/dzecniv][u/dzecniv]] - -> testing features along with workers and a file watcher? Shouldn't -they be different projects? - - What annoys you when developing in lisp? - -I find that setting up a test framework is more difficult than it -should be, so any effort on this area is appreciated. I mean: starting -with 5am is ok (but could be easier with an editor command), running -it from the CLI/a CI is less OK, getting the correct return code of -the tests needs more work, etc. - -* Protocols - -- [[https://chromedevtools.github.io/devtools-protocol/][Chrome DevTools Protocol]] -- Slime/Sly -- LSP (Language Server Protocol) -- LSIF (Language Server Index Format) -- Debug Adapter Protocol - -* To classify - -https://quickdocs.org/cl-scripting -https://quickdocs.org/repl-utilities -[[https://github.com/slime/slime/issues/532][slime issue #532: Rename package and all the symbol prefixes]] -https://blog.cddr.org/posts/2021-11-23-on-new-ides/ -https://common-lisp.net/project/slime/doc/html/Contributed-Packages.html - -https://quickdocs.org/external-symbol-not-found -https://github.com/Bike/compiler-macro -https://quickdocs.org/dotenv - -https://quickdocs.org/slite - SLIME based Test-runner for FiveAM tests -(and possibly others in the future) - -In SLIME's debugger, press ~v~ to navigate to its definition. - -https://github.com/melisgl/journal - for logging and trace-based -testing -https://github.com/melisgl/try/ - for a test framework that looks a -lot with what I want from a test framework. - -For a pretty nice review of existing testing framework: -https://sabracrolleton.github.io/testing-framework - -[[https://github.com/emacs-elsa/Elsa][Emacs Lisp Static Analyzer]] - -https://github.com/ruricolist/moira - Monitor and restart background threads. - -https://github.com/pokepay/cl-string-generator - Generate string from regular expression - -Emacs supports ~(declare (pure t) (side-effect-free t))~ - -[[https://github.com/programingship/common-lisp-sly][Sly with spacemacs]] - - -https://github.com/mmontone/duologue - High level user interaction library for Common Lis - -* Discord - -Discussion about =#:= -https://discord.com/channels/297478281278652417/569524818991644692/915330555334234192 - -* FAQ from newbies about common lisp - -** What's the difference between load and require? - -** What's asdf v. quicklisp v. packages v. "os packages"? - -** The heck is RPLACA? - -** What's the difference between =setf= and =setq=? - -https://stackoverflow.com/questions/869529/difference-between-set-setq-and-setf-in-common-lisp - -** Why use #:symbol (especially in =defpackage=)? - -** Why start a file with =(cl:in-package #:cl-user)=? - -** Why interactivity is important? - -They don't actually ask that, they usually just don't think or know -about it. - -Here's something that does an OK job at explaining the importance: -https://technotales.wordpress.com/2007/10/03/like-slime-for-vim/ - -** What's the difference between ~defvar~ and ~defparameter~? - -** Something about using ~setf~ to create variables... - -** A symbol can represent many things - -- variables/symbol macros -- functions/macros -- classes/conditions/types -- method combinations -- block names -- catch tags -- tagbody tags -- restarts -- packages -- compiler macros -- slot names -- compiler macros - -** When coming from another language - -*** How to create a function-local variable? - -** Proclaim v.s. Declaim v.s. Declare - -http://www.lispworks.com/documentation/lw50/LWUG/html/lwuser-90.htm - -** How packages and symbols works? - -https://flownet.com/ron/packages.pdf - -** Alternatives to the Hyperspec - -- [[http://clqr.boundp.org/download.html][Common Lisp Quick Reference]] -- Ultraspec (dead) -- Simplified something something -- The lisp cookbook - -** What the hell are pathnames? - -- Don't forget trailing backslashes for directories. - -** Where are the functions to operate on strings? - -- Use the functions that operate on sequences. -- Use libraries, like alexandria, split-sequences, serapeum, etc. - -* Glossary - -** lisp listener - -- More often called "lisp repl". -- I use this term to try to avoid confusion with an hypothetical - future actual REPL. -- You could describe that as a "client-server REPL". - -** REPl - -- Stands for "Read-Eval-Print-Loop" -- Most people think about "command line" when they hear REPL, but in - the case of lisp, it usually means a "listener". - -* Resources - -- [alexandria](https://alexandria.common-lisp.dev/draft/alexandria.html) -- [log4cl](https://github.com/7max/log4cl) - -* Local variables :noexport: -# local variables: -# org-confirm-babel-evaluate: nil -# end: diff --git a/scratch-files/archive/advise-swank.lisp b/scratch-files/archive/advise-swank.lisp new file mode 100644 index 00000000..229a9b77 --- /dev/null +++ b/scratch-files/archive/advise-swank.lisp @@ -0,0 +1,329 @@ + +(in-package :breeze.user) + +;; See https://bitbucket.org/budden/budden-tools/src/default/cl-advice/cl-advice.lisp + +(sb-int:encapsulated-p 'swank:eval-for-emacs 'default) + +(defun dump-form (form) + (breeze.utils:walk form (lambda (element) (cond + ((symbolp element) (format t " ~A:~A" + (package-name + (symbol-package element)) + (symbol-name element))) + (t (format t " ~a" element)))))) + +(defun handle-defun (form buffer-package id) + (format t "~&HANDLE-DEFUN")) + +(defun handle-progn (form buffer-package id) + (format t "~&HANDLE-progn")) + +(defun handle-event (event buffer-package id) + (alexandria:destructuring-case event + ((swank:interactive-eval form) + (format t "~&EVAL ~s~&" form) + (if (listp form) + (case (first form) + (defun (handle-defun form buffer-package id)) + (progn (handle-progn form buffer-package id)) + (t (format t "~&UNKNOWN ~A" (first form)))) + (format t "~&Not a list (~a) ~s~&" (type-of form) form))))) + +(defun 2x (x) (* x 2)) + +(+ 3 3) + +(handle-event '(hey) :packige 0) +(handle-event '(swank:interactive-eval 42) :packige 0) +(handle-event '(swank:interactive-eval (defun 2x (* 2 x))) :breeze.user 0) + +'swank-compile-string-for-emacs + +(defun my-eval-for-emacs (original-fn form buffer-package id) + (ignore-errors + (handler-bind + ((error #'(lambda (condition) + (format *error-output* "~&ERROR: ~A~&" condition)))) + + ;; (alexandria:destructuring-case form) + (format t "~&~%my-eval-for-emacs:~% * form: ~s~% * buffer-package: ~a~% * id: ~a~%" form buffer-package id)) + (handle-event form buffer-package id) + ;; (dump-form form) + ) + (funcall original-fn form buffer-package id)) + + +(sb-int:encapsulate 'swank:eval-for-emacs :default 'my-eval-for-emacs) +(sb-int:unencapsulate 'swank:eval-for-emacs :default) + +(apropos "encapsulate" 'sb-int t) + +(swank::dispatch-event (swank::default-connection) '(:write-string "hi")) + + +(apropos "" 'swank t) + +SWANK:*AFTER-TOGGLE-TRACE-HOOK* (bound) +SWANK:*BACKTRACE-PRINTER-BINDINGS* (bound) +SWANK:*COMMUNICATION-STYLE* (bound) +SWANK:*CONFIGURE-EMACS-INDENTATION* (bound) +SWANK:*DEDICATED-OUTPUT-STREAM-PORT* (bound) +SWANK:*DEFAULT-WORKER-THREAD-BINDINGS* (bound) +SWANK:*DONT-CLOSE* (bound) +SWANK:*FASL-PATHNAME-FUNCTION* (bound) +SWANK:*FIND-DEFINITIONS-LEFT-TRIM* (bound) +SWANK:*FIND-DEFINITIONS-RIGHT-TRIM* (bound) +SWANK:*FUZZY-DUPLICATE-SYMBOL-FILTER* (bound) +SWANK:*GLOBAL-DEBUGGER* (bound) +SWANK:*GLOBALLY-REDIRECT-IO* (bound) +SWANK:*INSPECTOR-VERBOSE* (bound) +SWANK:*LOG-EVENTS* (bound) +SWANK:*MACROEXPAND-PRINTER-BINDINGS* (bound) +SWANK:*READTABLE-ALIST* (bound) +SWANK:*RECORD-REPL-RESULTS* (bound) +SWANK:*SLDB-QUIT-RESTART* (bound) +SWANK:*SWANK-DEBUGGER-CONDITION* (bound) +SWANK:*SWANK-PPRINT-BINDINGS* (bound) +SWANK:*USE-DEDICATED-OUTPUT-STREAM* (bound) +SWANK:APROPOS-LIST-FOR-EMACS (fbound) +SWANK:ARGLIST-DISPATCH (fbound) +SWANK:AUTODOC (fbound) +SWANK:BACKTRACE (fbound) +SWANK:CLEAR-REPL-RESULTS (fbound) +SWANK:COMMIT-EDITED-VALUE (fbound) +SWANK:COMPILE-FILE-FOR-EMACS (fbound) +SWANK:COMPILE-FILE-IF-NEEDED (fbound) +SWANK:COMPILE-MULTIPLE-STRINGS-FOR-EMACS (fbound) +SWANK:COMPILE-STRING-FOR-EMACS (fbound) +SWANK:COMPLETE-FORM (fbound) +SWANK:COMPLETIONS (fbound) +SWANK:COMPLETIONS-FOR-CHARACTER (fbound) +SWANK:COMPLETIONS-FOR-KEYWORD (fbound) +SWANK:CONNECTION-INFO (fbound) +SWANK:CREATE-SERVER (fbound) +SWANK:DEBUG-NTH-THREAD (fbound) +SWANK:DEBUG-ON-SWANK-ERROR (fbound) +SWANK:DEBUGGER-INFO-FOR-EMACS (fbound) +SWANK:DESCRIBE-DEFINITION-FOR-EMACS (fbound) +SWANK:DESCRIBE-FUNCTION (fbound) +SWANK:DESCRIBE-INSPECTEE (fbound) +SWANK:DESCRIBE-SYMBOL (fbound) +SWANK:DISASSEMBLE-FORM (fbound) +SWANK:DOCUMENTATION-SYMBOL (fbound) +SWANK:ED-IN-EMACS (fbound) +SWANK:ED-RPC (fbound) +SWANK:ED-RPC-NO-WAIT (fbound) +SWANK:EVAL-AND-GRAB-OUTPUT (fbound) +SWANK:EVAL-FOR-EMACS (fbound) +SWANK:EVAL-IN-EMACS (fbound) +SWANK:EVAL-STRING-IN-FRAME (fbound) +SWANK:EXPORT-STRUCTURE (fbound) +SWANK:EXPORT-SYMBOL-FOR-EMACS (fbound) +SWANK:FIND-DEFINITION-FOR-THING (fbound) +SWANK:FIND-DEFINITIONS-FOR-EMACS (fbound) +SWANK:FIND-SOURCE-LOCATION-FOR-EMACS (fbound) +SWANK:FLOW-CONTROL-TEST (fbound) +SWANK:FRAME-LOCALS-AND-CATCH-TAGS (fbound) +SWANK:FRAME-PACKAGE-NAME (fbound) +SWANK:FROM-STRING (fbound) +SWANK:FUZZY-COMPLETION-SELECTED (fbound) +SWANK:FUZZY-COMPLETIONS (fbound) +SWANK:INIT-INSPECTOR (fbound) +SWANK:INIT-PRESENTATIONS (fbound) +SWANK:INSPECT-CURRENT-CONDITION (fbound) +SWANK:INSPECT-FRAME-VAR (fbound) +SWANK:INSPECT-IN-EMACS (fbound) +SWANK:INSPECT-IN-FRAME (fbound) +SWANK:INSPECT-NTH-PART (fbound) +SWANK:INSPECT-PRESENTATION (fbound) +SWANK:INSPECTOR-CALL-NTH-ACTION (fbound) +SWANK:INSPECTOR-EVAL (fbound) +SWANK:INSPECTOR-HISTORY (fbound) +SWANK:INSPECTOR-NEXT (fbound) +SWANK:INSPECTOR-NTH-PART (fbound) +SWANK:INSPECTOR-POP (fbound) +SWANK:INSPECTOR-RANGE (fbound) +SWANK:INSPECTOR-REINSPECT (fbound) +SWANK:INSPECTOR-TOGGLE-VERBOSE (fbound) +SWANK:INTERACTIVE-EVAL (fbound) +SWANK:INTERACTIVE-EVAL-REGION (fbound) +SWANK:INVOKE-NTH-RESTART (fbound) +SWANK:INVOKE-NTH-RESTART-FOR-EMACS (fbound) +SWANK:INVOKE-SLIME-DEBUGGER (fbound) +SWANK:IO-SPEED-TEST (fbound) +SWANK:KILL-NTH-THREAD (fbound) +SWANK:LIST-ALL-PACKAGE-NAMES (fbound) +SWANK:LIST-THREADS (fbound) +SWANK:LOAD-FILE (fbound) +SWANK:LOOKUP-AND-SAVE-PRESENTED-OBJECT-OR-LOSE (fbound) +SWANK:LOOKUP-PRESENTED-OBJECT (fbound) +SWANK:LOOKUP-PRESENTED-OBJECT-OR-LOSE (fbound) +SWANK:MAKE-OUTPUT-FUNCTION-FOR-TARGET (fbound) +SWANK:MAKE-OUTPUT-STREAM-FOR-TARGET (fbound) +SWANK:MOP (fbound) +SWANK:OPERATOR-ARGLIST (fbound) +SWANK:PACKAGE= (fbound) +SWANK:PARSE-STRING (fbound) +SWANK:PING (fbound) +SWANK:PPRINT-EVAL (fbound) +SWANK:PPRINT-EVAL-STRING-IN-FRAME (fbound) +SWANK:PPRINT-INSPECTOR-PART (fbound) +SWANK:PRINT-INDENTATION-LOSSAGE (fbound) +SWANK:PROFILE-BY-SUBSTRING (fbound) +SWANK:QUIT-INSPECTOR (fbound) +SWANK:QUIT-THREAD-BROWSER (fbound) +SWANK:RE-EVALUATE-DEFVAR (fbound) +SWANK:RESTART-SERVER (fbound) +SWANK:RUN-HOOK-WITH-ARGS-UNTIL-SUCCESS +SWANK:SDLB-PRINT-CONDITION (fbound) +SWANK:SET-PACKAGE (fbound) +SWANK:SIMPLE-BREAK (fbound) +SWANK:SIMPLE-COMPLETIONS (fbound) +SWANK:SLDB-ABORT (fbound) +SWANK:SLDB-BREAK (fbound) +SWANK:SLDB-BREAK-WITH-DEFAULT-DEBUGGER (fbound) +SWANK:SLDB-CONTINUE (fbound) +SWANK:SLDB-DISASSEMBLE (fbound) +SWANK:SLDB-NEXT (fbound) +SWANK:SLDB-OUT (fbound) +SWANK:SLDB-RETURN-FROM-FRAME (fbound) +SWANK:SLDB-STEP (fbound) +SWANK:START-SERVER (fbound) +SWANK:START-SWANK-SERVER-IN-THREAD (fbound) +SWANK:STARTUP-MULTIPROCESSING +SWANK:STOP-SERVER (fbound) +SWANK:SWANK-COMPILER-MACROEXPAND (fbound) +SWANK:SWANK-COMPILER-MACROEXPAND-1 (fbound) +SWANK:SWANK-DEBUGGER-HOOK (fbound) +SWANK:SWANK-DELETE-PACKAGE (fbound) +SWANK:SWANK-EXPAND (fbound) +SWANK:SWANK-EXPAND-1 (fbound) +SWANK:SWANK-FORMAT-STRING-EXPAND (fbound) +SWANK:SWANK-MACROEXPAND (fbound) +SWANK:SWANK-MACROEXPAND-1 (fbound) +SWANK:SWANK-MACROEXPAND-ALL (fbound) +SWANK:SWANK-PROFILE-PACKAGE (fbound) +SWANK:SWANK-REQUIRE (fbound) +SWANK:SWANK-TOGGLE-TRACE (fbound) +SWANK:THROW-TO-TOPLEVEL (fbound) +SWANK:TO-STRING (fbound) +SWANK:TOGGLE-BREAK-ON-SIGNALS (fbound) +SWANK:TOGGLE-DEBUG-ON-SWANK-ERROR (fbound) +SWANK:TOGGLE-PROFILE-FDEFINITION (fbound) +SWANK:UNDEFINE-FUNCTION (fbound) +SWANK:UNEXPORT-SYMBOL-FOR-EMACS (fbound) +SWANK:UNINTERN-SYMBOL (fbound) +SWANK:UNREADABLE-RESULT +SWANK:UNREADABLE-RESULT-P (fbound) +SWANK:UNREADABLE-RESULT-STRING (fbound) +SWANK:UNTRACE-ALL (fbound) +SWANK:UPDATE-INDENTATION-INFORMATION (fbound) +SWANK:VALUE-FOR-EDITING (fbound) +SWANK:XREF (fbound) +SWANK:XREFS (fbound) +SWANK:Y-OR-N-P-IN-EMACS (fbound) + + + +(swank:y-or-n-p-in-emacs "Do you?") + + + + +(ppcre:regex-apropos-list "^\\*.*\\*$" :swank) + +SWANK/BACKEND:*AUTO-FLUSH-INTERVAL* +SWANK/BACKEND:*DEBUG-SWANK-BACKEND* +SWANK/BACKEND:*INTERRUPT-QUEUED-HANDLER* +SWANK/BACKEND:*LOG-OUTPUT* +SWANK/BACKEND:*PENDING-SLIME-INTERRUPTS* + +SWANK:*AFTER-TOGGLE-TRACE-HOOK* +SWANK:*BACKTRACE-PRINTER-BINDINGS* +SWANK:*COMMUNICATION-STYLE* +SWANK:*CONFIGURE-EMACS-INDENTATION* +SWANK:*DEDICATED-OUTPUT-STREAM-PORT* +SWANK:*DEFAULT-WORKER-THREAD-BINDINGS* +SWANK:*DONT-CLOSE* +SWANK:*FASL-PATHNAME-FUNCTION* +SWANK:*FIND-DEFINITIONS-LEFT-TRIM* +SWANK:*FIND-DEFINITIONS-RIGHT-TRIM* +SWANK:*FUZZY-DUPLICATE-SYMBOL-FILTER* +SWANK:*GLOBAL-DEBUGGER* +SWANK:*GLOBALLY-REDIRECT-IO* +SWANK:*INSPECTOR-VERBOSE* +SWANK:*LOG-EVENTS* +SWANK:*MACROEXPAND-PRINTER-BINDINGS* +SWANK:*READTABLE-ALIST* +SWANK:*RECORD-REPL-RESULTS* +SWANK:*SLDB-QUIT-RESTART* +SWANK:*SWANK-DEBUGGER-CONDITION* +SWANK:*SWANK-PPRINT-BINDINGS* +SWANK:*USE-DEDICATED-OUTPUT-STREAM* + +SWANK::*AFTER-INIT-HOOK* +SWANK::*ALL-CHUNKS* +SWANK::*ARGLIST-PPRINT-BINDINGS* +SWANK::*ARGLIST-SHOW-PACKAGES* +SWANK::*AUTO-ABBREVIATE-DOTTED-PACKAGES* +SWANK::*BACKTRACE-PPRINT-DISPATCH-TABLE* +SWANK::*BUFFER-PACKAGE* +SWANK::*BUFFER-READTABLE* +SWANK::*CANONICAL-PACKAGE-NICKNAMES* +SWANK::*CHANNEL-COUNTER* +SWANK::*CHANNELS* +SWANK::*COMPILE-FILE-FOR-EMACS-HOOK* +SWANK::*CONNECTION-CLOSED-HOOK* +SWANK::*CONNECTIONS* +SWANK::*CURRENT-DEBUG-IO* +SWANK::*CURRENT-ERROR-OUTPUT* +SWANK::*CURRENT-QUERY-IO* +SWANK::*CURRENT-STANDARD-INPUT* +SWANK::*CURRENT-STANDARD-OUTPUT* +SWANK::*CURRENT-TERMINAL-IO* +SWANK::*CURRENT-TRACE-OUTPUT* +SWANK::*DEBUG-ON-SWANK-PROTOCOL-ERROR* +SWANK::*ECHO-AREA-PREFIX* +SWANK::*EMACS-CONNECTION* +SWANK::*ENABLE-EVENT-HISTORY* +SWANK::*EVENT-HISTORY* +SWANK::*EVENT-HISTORY-INDEX* +SWANK::*EVENT-HOOK* +SWANK::*FIND-MODULE* +SWANK::*FUZZY-COMPLETION-SYMBOL-PREFIXES* +SWANK::*FUZZY-COMPLETION-SYMBOL-SUFFIXES* +SWANK::*FUZZY-COMPLETION-WORD-SEPARATORS* +SWANK::*FUZZY-RECURSION-SOFT-LIMIT* +SWANK::*GF-METHOD-GETTER* +SWANK::*INSPECTOR-HISTORY* +SWANK::*INSPECTOR-PRINTER-BINDINGS* +SWANK::*INSPECTOR-SLOTS-DEFAULT-GROUPING* +SWANK::*INSPECTOR-SLOTS-DEFAULT-ORDER* +SWANK::*INSPECTOR-VERBOSE-PRINTER-BINDINGS* +SWANK::*IO-INTERUPT-LEVEL* +SWANK::*ISTATE* +SWANK::*LOAD-PATH* +SWANK::*LOOPBACK-INTERFACE* +SWANK::*NEW-CONNECTION-HOOK* +SWANK::*NIL-SURROGATE* +SWANK::*OBJECT-TO-PRESENTATION-ID* +SWANK::*PENDING-CONTINUATIONS* +SWANK::*PRE-REPLY-HOOK* +SWANK::*PRESENTATION-ACTIVE-MENU* +SWANK::*PRESENTATION-COUNTER* +SWANK::*PRESENTATION-ID-TO-OBJECT* +SWANK::*SEND-COUNTER* +SWANK::*SERVERS* +SWANK::*SLDB-CONDITION-PRINTER* +SWANK::*SLDB-INITIAL-FRAMES* +SWANK::*SLDB-LEVEL* +SWANK::*SLDB-RESTARTS* +SWANK::*SLDB-STEPPING-P* +SWANK::*SLIME-FEATURES* +SWANK::*SLIME-INTERRUPTS-ENABLED* +SWANK::*SWANK-DEBUG-P* +SWANK::*SWANK-IO-PACKAGE* +SWANK::*SWANK-WIRE-PROTOCOL-VERSION* +SWANK::*TAG-COUNTER* +SWANK::*THREAD-LIST* diff --git a/scratch-files/archive/annotation.el b/scratch-files/archive/annotation.el new file mode 100644 index 00000000..ba9786f8 --- /dev/null +++ b/scratch-files/archive/annotation.el @@ -0,0 +1,113 @@ +;; Using emacs to edit images?? + +(defvar *demo-root* (concat + (vc-find-root + (buffer-file-name + (current-buffer)) + ".git") + "/demo")) + +(defvar *demo-annotations* ()) + +(defun demo-frame-annotation (svg) + (assoc-string + svg + *demo-annotations*)) + +(defun demo-annotate-frame (root svg) + (save-window-excursion + ;; (toggle-frame-fullscreen ) + (save-excursion + (let ((buffer + (find-file (concat root "/" svg)))) + (unwind-protect + (progn + (delete-other-windows) + (with-current-buffer buffer + (let* ((size (image-size (image--get-image))) + (width (car size)) + (height (cdr size)) + (scale (min (/ (window-width) width) + (/ (window-height) height)))) + (image--change-size scale))) + (push + (cons svg + (read-string "Annotate this frame: ")) + *demo-annotations*)) + (kill-buffer buffer)))))) + +(defun demo-annotate-all-frames-in-folder (root) + (cl-loop for svg in (directory-files root nil "\\.svg$") + repeat 1 + unless (demo-frame-annotation svg) + do (demo-annotate-frame root svg))) + +(demo-annotate-all-frames-in-folder + (concat + *demo-root* + "/annotated")) + + + + +(defun demo-read-annotation-from-text-file (file) + (cl-loop for line in + (split-string + (with-temp-buffer + (insert-file-contents file) + (buffer-substring-no-properties (point-min) (point-max))) + "\n" + t) + collect + (save-match-data + (and (string-match + "^\\([a-z0-9]+\\) \\([^ ]+\\) ?\\(.*\\)$" line) + (let ((filename + (match-string 2 line)) + (hash (match-string 1 line)) + (annotation (match-string 3 line))) + (list + filename + :sha1 hash + :annotation + (if (string-empty-p annotation) + ;; Get from global variable + (getf + (cdr (demo-frame-annotation + (match-string 2 line))) + :annotation) + annotation))))))) + + + +(defvar *demo-annotation-from-file* + (let* ((root (concat + *demo-root* + "/annotated")) + (file (concat root "/annotations.txt"))) + (demo-read-annotation-from-text-file file))) + + +(getf + (cdr + (assoc-string "termtosvg_00001.svg" + *demo-annotation-from-file*)) + :annotation) + +;; (setf *demo-annotations* *demo-annotation-from-file*) +;; (setf *demo-annotation-from-file* *demo-annotations*) + +;; Write it back +(let* ((root (concat + *demo-root* + "/annotated")) + (file (concat root "/annotations2.txt"))) + (with-temp-buffer + (cl-loop for (filename _ hash _ annotation) in *demo-annotation-from-file* + do (insert hash " " filename " " annotation "\n")) + (when (file-writable-p file) + (write-region (point-min) + (point-max) + file)) + (buffer-substring-no-properties (point-min) (point-max)) + )) diff --git a/scratch-files/archive/channel-for-commands.lisp b/scratch-files/archive/channel-for-commands.lisp new file mode 100644 index 00000000..f0531875 --- /dev/null +++ b/scratch-files/archive/channel-for-commands.lisp @@ -0,0 +1,80 @@ +(cl:in-package #:common-lisp-user) + +(defpackage #:channel-for-commands + (:use #:cl) + (:local-nicknames + (#:a #:alexandria) + (#:bt #:bordeaux-threads) + (#:c #:chanl))) + +(in-package #:channel-for-commands) + + +;;; I realized that chanl could be used instead of the annoying CPS +;;; (continuation passing style) stuff to implement the commands, +;;; let's try it! +;;; +;;; First thing, we need to create a channel and a thread, and use the +;;; channel. + +(let ((channel (make-instance 'c:channel))) + (bt:make-thread + #'(lambda () + (let ((x (c:recv channel))) + (c:send channel (* 2 x)))) + :name "breeze command") + (c:send channel 42) + (c:recv channel)) + + +;;; Right away, I can think of 2 little utilities that could help. + +(defparameter *channel* nil + "The channel used for interacting with commands.") + +(defun call (&rest args) + "Send ARGS to *channel* and wait for a return value." + (c:send *channel* args) + (c:recv *channel*)) + +(defmacro handle (lambda-list &body body) + "Wait for a message from *channel*, process it and return a the +result of the last form on *channel*." + `(destructuring-bind ,lambda-list + (c:recv *channel*) + (c:send *channel* + (progn + ,@body)))) + +;;; And we can use c:pcall as a drop-in replacement for br:make-thread +;;; it'll return a nicer object. + +(let* ((*channel* (make-instance 'c:channel))) + (c:pcall + #'(lambda () + (handle (x) + (* 2 x))) + :name "breeze command" + :initial-bindings (acons '*channel* + (list *channel*) + c:*default-special-bindings*)) + (call 42)) + +;;; And yest another helper-macro + +(defmacro tasklet (() &body body) + "Creates a thread, return a channel to communicate with it." + (a:with-gensyms (channel) + `(let* ((,channel (make-instance 'c:channel))) + (c:pcall + #'(lambda () + (let ((*channel* ,channel)) + ,@body)) + :name "breeze command") + ,channel))) + +;; Now it seems much more user-friendly than CPS! +(let ((*channel* (tasklet () + (handle (x) + (* 2 x))))) + (call 42)) diff --git a/scratch-files/archive/definition.lisp b/scratch-files/archive/definition.lisp new file mode 100644 index 00000000..57963f8f --- /dev/null +++ b/scratch-files/archive/definition.lisp @@ -0,0 +1,73 @@ + +(defpackage #:breeze.definition + (:documentation "Provides replacements for \"definition forms\" (such as defun and defmacro). +The goal is to (portably) make sure we keep the definitions and not just their [compiled] results.") + (:use :cl) + (:shadow cl:defun cl:fmakunbound) + (:export + #:*function* + #:*function-redifinition-hooks* + #:defun + #:fmakunbound + #:function-body)) + +(in-package #:breeze.definition) + +(defvar *function* (make-hash-table) + "Set of all functions defined with breeze.definition:defun") + +(defvar *function-redifinition-hooks* () + "List of functions to call when a function is redefined") + +(cl:defun flag-funtion-redifinition (name) + "Calls each function in *funtion-redifinition-hooks*." + (loop :for hook :in *function-redifinition-hooks* + :do (funcall hook name))) + +(defmacro defun (&whole whole name lambda-list &body body) + "Define a functions and saves its definition in memory, flag a function redefinition." + `(progn (cl:defun ,name ,lambda-list + ,@body) + (setf (gethash ',name *function*) ',whole) + (flag-funtion-redifinition ',name) + ',name)) + +(cl:defun fmakunbound (name) + "Make NAME have no global function definition." + (cl:fmakunbound name) + (remhash name *function*)) + +(cl:defun function-body (name) + "Get the body of a function by name" + (cdddr (gethash name *function*))) + +;; TODO defmacro +;; TODO defgeneric +;; TODO defmethod +;; TODO defclass +;; TODO what about closures? + +#| +(loop :for symbol :being :the :external-symbol :of :cl +:when (alexandria:starts-with-subseq "DEF" (symbol-name symbol)) +:collect symbol) + +(DEFINE-SYMBOL-MACRO +DEFCLASS +DEFUN +DEFSETF +DEFMETHOD +DEFCONSTANT +DEFINE-METHOD-COMBINATION +DEFSTRUCT +DEFVAR +DEFGENERIC +DEFTYPE +DEFMACRO +DEFPARAMETER +DEFPACKAGE +DEFINE-COMPILER-MACRO +DEFINE-MODIFY-MACRO +DEFINE-CONDITION +DEFINE-SETF-EXPANDER) +|# diff --git a/scratch-files/archive/try-40ants-ci.lisp b/scratch-files/archive/try-40ants-ci.lisp new file mode 100644 index 00000000..4ed7db26 --- /dev/null +++ b/scratch-files/archive/try-40ants-ci.lisp @@ -0,0 +1,184 @@ + +(defpackage #:breeze.try-40ants-ci + (:use #:cl) + (:documentation "")) + +(in-package #:breeze.try-40ants-ci) + +(ql:system-apropos-list "40ants") +#| +(# +# +#) +|# + +;;; cloned https://github.com/40ants/ci in local-projects + +(ql:quickload "40ants-ci") + +;;; System "docs-config" not found, it needs more stuff (that are in +;;; UltraLisp...) + + +(ql-dist:install-dist "http://dist.ultralisp.org/" + :prompt nil) + +;; Let's try again +(ql:quickload "40ants-ci") + + +(40ants-ci/workflow:defworkflow test + :on-pull-request t + :jobs ((40ants-ci/jobs/linter:linter))) + +;;; So, it's trying to find a system with the same name as the package +;;; Which is was the doc says: "In next examples, I'll presume you are +;;; writing code in a file which is the part of the package inferred +;;; ASDF system [...]" + +#| +Component "breeze.try-40ants-ci" not found + [Condition of type ASDF/FIND-COMPONENT:MISSING-COMPONENT] + +Restarts: + 0: [RETRY] Retry ASDF operation. + 1: [CLEAR-CONFIGURATION-AND-RETRY] Retry ASDF operation after resetting the configuration. + 2: [ABORT] Abort compilation. + 3: [*ABORT] Return to SLIME's top level. + 4: [ABORT] abort thread (#) + +Backtrace: + 0: ((LAMBDA NIL :IN ASDF/SYSTEM:FIND-SYSTEM)) + 1: (ASDF/SESSION:CONSULT-ASDF-CACHE (ASDF/SYSTEM:FIND-SYSTEM "breeze.try-40ants-ci") #) + 2: (ASDF/SESSION:CALL-WITH-ASDF-SESSION # :OVERRIDE NIL :KEY (ASDF/SYSTEM:FIND-SYSTEM "breeze.try-40ants-ci") :OVERRIDE-CACHE NIL :OVERRIDE.. + 3: ((:METHOD 40ANTS-CI/WORKFLOW::ON-WORKFLOW-REDEFINITION (40ANTS-CI/WORKFLOW::WORKFLOW)) #) [fast-method] + 4: (SB-FASL::LOAD-FASL-GROUP #S(SB-FASL::FASL-INPUT :STREAM # :TABLE #(47 # SB-PCL::LOAD-DEFCLASS # NIL NIL) + 6: ((LABELS SB-FASL::LOAD-STREAM-1 :IN LOAD) # T) + 7: (SB-FASL::CALL-WITH-LOAD-BINDINGS # # T #) + 10: ((FLET SWANK/BACKEND:SWANK-COMPILE-STRING :IN "/usr/home/fstamour/.config/stumpwm/slime/swank/sbcl.lisp") "(40ants-ci/workflow:defworkflow test ..) + 11: ((LAMBDA NIL :IN SWANK:COMPILE-STRING-FOR-EMACS)) + 12: ((LAMBDA NIL :IN SWANK::COLLECT-NOTES)) + 13: (SWANK::MEASURE-TIME-INTERVAL #) + 14: (SWANK::COLLECT-NOTES #) + 15: (SWANK::CALL-WITH-BUFFER-SYNTAX NIL #) + 16: (SB-INT:SIMPLE-EVAL-IN-LEXENV (SWANK:COMPILE-STRING-FOR-EMACS "(40ants-ci/workflow:defworkflow test ..) + 17: (EVAL (SWANK:COMPILE-STRING-FOR-EMACS "(40ants-ci/workflow:defworkflow test ..) + 18: (SWANK:EVAL-FOR-EMACS (SWANK:COMPILE-STRING-FOR-EMACS "(40ants-ci/workflow:defworkflow test ..) + 19: ((LAMBDA NIL :IN SWANK::SPAWN-WORKER-THREAD)) + 20: (SWANK/SBCL::CALL-WITH-BREAK-HOOK # #) + 21: ((FLET SWANK/BACKEND:CALL-WITH-DEBUGGER-HOOK :IN "/usr/home/fstamour/.config/stumpwm/slime/swank/sbcl.lisp") # #)) #) + 23: ((LAMBDA NIL :IN SWANK::SPAWN-WORKER-THREAD)) + 24: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN)) + 25: ((FLET "WITHOUT-INTERRUPTS-BODY-11" :IN SB-THREAD::RUN)) + 26: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN)) + 27: ((FLET "WITHOUT-INTERRUPTS-BODY-4" :IN SB-THREAD::RUN)) + 28: (SB-THREAD::RUN) + 29: ("foreign function: call_into_lisp") + 30: ("foreign function: funcall1") +|# + + +;;; Lookint at the macro expension +;;; - it creates class named TEST (the name of the workflow) +;;; - it makes a list of object of type Job, then +;;; - create an instance of the class TEST +;;; - register that instance +;;; - calls the method on-workflow-redefinition with that instance +;;; +;;; It's the last call (on-workflow-redefinition) that trips my test +;;; here. The code heavily assume that you use ASDF's +;;; package-inferred-system. + + +;;; We can still reach into 40ants/ci's guts + +(defparameter *wf* + (gethash 'test + (gethash (package-name *package*) + 40ants-ci/workflow::*registered-workflows*)) +"The workflow object that was just created, but failed to generate (in on-workflow-redefinition).") + +(let ((40ants-ci/vars:*current-system* (asdf:find-system 'breeze))) + (40ants-ci/github:generate + *wf* + (breeze.utils:breeze-relative-pathname ".github/workflows/linter.yml"))) + + +;;; ^ That hacky stuff worked, let's try something better +;;; I'm getting tired, so the prose is going away. + +(defgeneric workflow-system (workflow)) + +(defmethod workflow-system ((workflow 40ants-ci/workflow::workflow)) + (asdf:find-system + (asdf:primary-system-name + (string-downcase + (package-name *package*))))) + +(defclass system-based-workflow () ; not a good name... + ((system :reader workflow-system))) + +(defclass breeze-workflow (system-based-workflow) + ((system :initform (asdf:find-system 'breeze)))) + +(defclass linter (breeze-workflow) ()) + +(workflow-system (make-instance 'breeze-workflow)) +;; => # + +(defun on-workflow-redefinition* (workflow) + (let* ((system (print (breeze.try-40ants-ci::workflow-system workflow) *debug-io*)) + (system-path (asdf:system-relative-pathname system "")) + (40ants-ci/vars:*current-system* system) + (workflow-path + (40ants-ci/workflow::make-workflow-path system-path workflow))) + (40ants-ci/github:generate workflow workflow-path) + )) + +(defmacro defworkflow (name &key + on-push-to + by-cron + on-pull-request + cache + jobs + superclasses) + `(progn + (defclass ,name (workflow ,@superclasses) + ()) + (let* ((jobs (mapcar #'make-job ',jobs)) + (workflow (make-instance ',name + :name ',name + :jobs jobs + :on-push-to ',(uiop:ensure-list on-push-to) + :by-cron ',(uiop:ensure-list by-cron) + :on-pull-request ,on-pull-request + :cache ,cache))) + (register-workflow workflow) + (on-workflow-redefinition* workflow) + workflow))) + + +(in-package #:breeze.try-40ants-ci) + +(40ants-ci/workflow:defworkflow linter + :superclasses (breeze-workflow) + :on-pull-request t + :jobs ((40ants-ci/jobs/linter:linter))) + +;;; For some reason (that I'm too tired to figure out), it is still +;;; calling the original on-workflow-redefinition... I should check +;;; the call sack to see if it's called from somewhere else... But it +;;; didn't work even when I had redefined the exact same method (same +;;; specializers). + +;;; Uninstall ultralisp +#+ (or) +(ql-dist:uninstall + (find-if #'(lambda (dist) + (string= "ultralisp" + (ql-dist:name dist))) + (ql-dist:all-dists))) diff --git a/scratch-files/bundle.lisp b/scratch-files/bundle.lisp new file mode 100644 index 00000000..d45b31d2 --- /dev/null +++ b/scratch-files/bundle.lisp @@ -0,0 +1,28 @@ +;;; Trying to bundle breeze's source code in one big file +#| + +Which makes me think: it would be nice to have some kind +to "transactions" when loading common lisp code. To avoid half-loaded +code. + +EDIT before I commit: I didn't have much time and didn't get far. BUT, +I managed to figure out that (surprise surprise, the dependencies are +hard to handle correctly...). + +|# + +;; Making sure the breeze system can be found. +(asdf:locate-system "breeze") + +(defparameter *concat* + (multiple-value-list + (asdf:operate 'asdf:concatenate-source-op "breeze"))) + +(defparameter *bundle* + (multiple-value-list + (asdf:perform 'asdf:concatenate-source-op "breeze"))) + +(first *bundle*) +#P"/home/fstamour/.cache/common-lisp/sbcl-2.4.0-linux-x64/home/fstamour/dev/breeze/src/breeze--system.lisp" + +(load (first *bundle*)) diff --git a/scratch-files/compile.el b/scratch-files/compile.el new file mode 100644 index 00000000..362883e8 --- /dev/null +++ b/scratch-files/compile.el @@ -0,0 +1,69 @@ +;;; Trying out emacs' "compile" command +;;; + + +;; Trying out with parachute's output + + +(cl-defun breeze--convert-compilation-error-to-buffer-or-file () + (message "breeze--convert-compilation-error-to-buffer-or-file: %S" + (list (current-buffer) + (point) + (buffer-substring-no-properties + (match-beginning 1) + (match-end 1)))) + (list "/home/fstamour/quicklisp/local-projects/breeze/") + ;; (list ".../breeze/src/lossless-reader.lisp") + ) + +(cl-defun breeze--convert-compilation-error-to-buffer-or-file () + ;; (message "breeze--convert-compilation-error-to-buffer-or-file: %S" (list (current-buffer) (point))) + (let ((symbol (buffer-substring-no-properties + (match-beginning 1) + (match-end 1)))) + ;; TODO handle multiple definitions + (save-match-data + (let* ((definition (first (slime-find-definitions symbol))) + (location-alist (cdadr definition)) + (file (car (alist-get :file location-alist)))) + (message "breeze--: %S %S" symbol file) + (unless file + (let* ((parts (split-string symbol "::")) + (needle (downcase (or (cadr parts) (car parts))))) + (deadgrep needle nil))) + file)))) + +(push '("tests failed in \\(.*\\)$" breeze--convert-compilation-error-to-buffer-or-file) + compilation-error-regexp-alist) + +(trace-function-background 'breeze--convert-compilation-error-to-buffer-or-file) +(trace-function-background 'compilation-error-properties) +(trace-function-background 'compilation-internal-error-properties) + +(save-excursion + (with-current-buffer (get-buffer-create "*scratch-compile-mode-parachute*") + (erase-buffer) + (insert " + 0/ 0 tests failed in BREEZE.TEST.LOSSLESS-READER::READ-PUNCTUATION +The test failed to evaluate properly: + The function BREEZE.TEST.LOSSLESS-READER::REGISTER-TEST-STRING is undefined. + 0/ 0 tests failed in BREEZE.TEST.LOSSLESS-READER::UNPARSE +The test failed to evaluate properly: + The function BREEZE.TEST.LOSSLESS-READER::REGISTER-TEST-STRING is undefined. +") + (compilation-parse-errors (point-min) (point-max)))) + +(cl-loop for symbol in '("BREEZE.TEST.LOSSLESS-READER::REGISTER-TEST-STRING" + "REGISTER-TEST-STRING") + for splitted = (split-string symbol "::") + collect (or (cadr splitted) (car splitted))) + +(split-string + "REGISTER-TEST-STRING" + "::") + + +;; It's not straightforward to figure out _where_ a missing function is missing xD +(push '("The function \\(.*\\) is undefined." breeze--convert-compilation-error-to-buffer-or-file) compilation-error-regexp-alist) + +(pop compilation-error-regexp-alist) diff --git a/src/reader.lisp b/scratch-files/eclector/reader.lisp similarity index 100% rename from src/reader.lisp rename to scratch-files/eclector/reader.lisp diff --git a/tests/reader.lisp b/scratch-files/eclector/reader.test.lisp similarity index 100% rename from tests/reader.lisp rename to scratch-files/eclector/reader.test.lisp diff --git a/src/syntax-tree.lisp b/scratch-files/eclector/syntax-tree.lisp similarity index 100% rename from src/syntax-tree.lisp rename to scratch-files/eclector/syntax-tree.lisp diff --git a/scratch-files/fancy-emacs-buffers.el b/scratch-files/fancy-emacs-buffers.el new file mode 100644 index 00000000..487edbaa --- /dev/null +++ b/scratch-files/fancy-emacs-buffers.el @@ -0,0 +1,180 @@ +;;; -*- lexical-binding: t; -*- + +;;; Trying out/taking notes to make a "fancy" UI + +(defmacro breeze-comment (&rest _)) + +(breeze-comment + ;; See overlay properties and breeze-overlay-put-modifications-hooks + (defun breeze-modification-hook (overlay afterp beg end &optional pre-change-length) + (let ((content (buffer-substring-no-properties + (overlay-start overlay) + (overlay-end overlay)))) + (if afterp + (message "After %S" content) + (message "Before: %S" content))))) + +;; See overlay properties and breeze-overlay-put-modifications-hooks* +(defun breeze-modification-hook (overlay afterp beg end &optional pre-change-length) + (let* ((content (buffer-substring-no-properties + (overlay-start overlay) + (overlay-end overlay))) + (transform (overlay-get overlay 'breeze-transform)) + (target (overlay-get overlay 'breeze-target)) + (inhibit-read-only t) + (buffer-undo-list t) + (beg (overlay-start target)) + (end (overlay-end target))) + (save-excursion + (goto-char beg) + (delete-char (- end beg)) + ;; TODO add error-handling around the funcall + (insert (propertize (funcall transform content) 'read-only t))))) + +;; Trying my best xD +(defun breeze-reset-buffer (name) + (let ((inhibit-read-only t) + (inhibit-modification-hooks t)) + (when-let ((buffer (get-buffer name))) + (with-current-buffer buffer + (remove-overlays) + (erase-buffer) + (kill-all-local-variables t)) + ;; (kill-buffer buffer) + )) + (get-buffer-create name)) + +;; (let ((buffer (breeze-reset-buffer "test"))) (display-buffer buffer)) + +(cl-defun breeze-insert (string) + (when string + (if (listp string) + (insert (apply 'propertize string)) + (insert string)))) + +;; TODO perhaps not the best name... +(cl-defun breeze-make-overlay-with-text + (&optional before inside after front-advance (rear-advance t)) + "Insert BEFORE, INSIDE and AFTER; then create an overlay that spans +the same range as INSIDE." + (breeze-insert before) + (let ((start (point))) + (breeze-insert inside) + (let ((end (point))) + (breeze-insert after) + (make-overlay start end (current-buffer) + front-advance rear-advance)))) + +(defun breeze-overlay-put-modifications-hooks (overlay) + "Setup the OVERLAY's hooks to call breeze-modification-hook on any +kind of modifications." + ;; With overlays, these hooks are called both before and after + ;; each change. + (dolist (property '(modification-hooks + insert-in-front-hooks + insert-behind-hooks)) + (overlay-put overlay property (list 'breeze-modification-hook)))) + +(cl-defun breeze-display-buffer (&optional pop) + (if pop + (pop-to-buffer (current-buffer)) + (display-buffer (current-buffer) + 'display-buffer-same-window))) + +(defface breeze-section-heading + (let ((common '( :extend t + :weight bold + :inherit separator-line + :height 1.2 + :underline t))) + `((((class color) (background light)) + ,@common + :foreground "DarkGoldenrod4") + (((class color) (background dark)) + ,@common + :foreground "LightGoldenrod2"))) + "Headings" + ;; TODO :group + ) + +;; (face-all-attributes 'breeze-section-heading) + +(defun breeze-overlay-put* (overlay props) + (cl-loop for (name value) on props by 'cddr + do (overlay-put overlay name value))) + + +(let ((buffer (breeze-reset-buffer "*stupid-tests*")) + (transform (lambda (string) + (string-reverse string)))) + (with-current-buffer buffer + (breeze-display-buffer) + ;; (setq buffer-read-only t) + (let* ((inhibit-read-only t) + (buffer-undo-list t) + (heading-props `( read-only t + front-sticky t + face breeze-section-heading)) + (overlay1 (breeze-make-overlay-with-text + `( "Input\n" ,@heading-props rear-nonsticky t) + "" + `("\n" read-only t))) + (overlay2 (breeze-make-overlay-with-text + `( "Output\n" ,@heading-props )))) + (breeze-overlay-put-modifications-hooks overlay1) + (breeze-overlay-put* overlay1 + `( breeze-transform ,transform + breeze-target ,overlay2)) + (breeze-overlay-put* overlay2 `(font-lock-face bold)) + (progn ; save-excursion + (goto-char (overlay-start overlay1)) + (insert "test"))))) + + +(breeze-comment + (let ((str "hello")) + (add-text-properties + 0 (length str) + `(,@nil + show-help-function nil + intangible nil ;; this is obsolete + inhibit-isearch nil + read-only t + mouse-face highlight + rear-nonsticky t + face font-lock-doc-face) + str) + (insert str))) + + +;; (make-overlay beg end &optional buffer front-advance rear-advance) +;; (overlay-put overlay prop value) + +;; (overlay-at pos) +;; (overlays-in beg end) +;; (overlays-list) +;; (overlay-properties) + +;; (mapcar 'overlay-properties (overlays-at (point))) + +;; The 'evaporate property is "crucial" for non-temporary buffers + +;; Important note: (erase-buffer) doesn't remove overlays... + + + +;; (put-text-property ) + + +;;; Takeaways +;; - pop-up or display a buffer +;; - reset/clean the buffer +;; - sprinkle with overlays or text properties +;; - overlays are not usually deleted automatically + + + +;; (thing-at-point THING &optional NO-PROPERTIES) +;; ‘symbol’, ‘list’, ‘sexp’, ‘defun’, +;; ‘filename’, ‘existing-filename’, ‘url’, ‘email’, ‘uuid’, ‘word’, +;; ‘sentence’, ‘whitespace’, ‘line’, ‘number’, ‘face’ and ‘page’. diff --git a/scratch-files/inspector.lisp b/scratch-files/inspector.lisp new file mode 100644 index 00000000..f41c8164 --- /dev/null +++ b/scratch-files/inspector.lisp @@ -0,0 +1,350 @@ +;;; Exploration: trying to generate a "place expression" from the +;;; current state of slime's inspector. +;;; +;;; Idea from @Akasha on discord: +;;; https://discord.com/channels/297478281278652417/297478350145060875/1183809496087277663 +#| + +@Akasha wanted something to generate a form when opening the inspector +from the debugger, but I'll start simpler: inspecting a very simple +expression, like a symbol... + +|# + +(in-package :cl-user) + +(require 'alexandria) + +(eval-when (:load-toplevel :execute) + (setq *print-circle* t)) + +;; Let's create a dummy value to inspect +(defparameter *x* (alexandria:plist-hash-table + `(:a "asdf" + :b (a (nested (list #(with-an-array 42)))) + :c ,(cons :x :y) + ;; :circular #1=(x . #1#) + :nested ,(alexandria:plist-hash-table + `( + 1 "un" + 2 "deux" + 3 "trois")) + :improper-list ,(list* 'a 'b 'c) + ))) + + + +#+ elisp +(progn + (define-key lisp-mode-map (kbd "M-c") #'eval-defun) + (define-key lisp-mode-map (kbd "C-c C-f") + (lambda () + (interactive) + (let ((slime-buffer-package "CL-USER")) + (slime-inspect "*x*"))))) + + +(in-package :swank) + +;;; Inspecting the inspector's state + +#++ +(progn + (istate.object *istate*) + ;; => # + + + (istate.content *istate*) + #| + ( + "Count" ": " (:VALUE 2) (:NEWLINE) + "Size" ": " (:VALUE 7) (:NEWLINE) "Test" + ": " (:VALUE EQL) (:NEWLINE) + "Rehash size" ": " (:VALUE 1.5) (:NEWLINE) + "Rehash threshold" ": " (:VALUE 1.0) (:NEWLINE) + (:ACTION "[clear hashtable]" + #) + + (:NEWLINE) "Contents: " (:NEWLINE) + + (:VALUE :A) " = " (:VALUE "asdf") " " + (:ACTION "[remove entry]" + #) (:NEWLINE) + + (:VALUE :B) " = " (:VALUE (A (NESTED (LIST #(WITH-AN-ARRAY 42))))) + " " + (:ACTION "[remove entry]" + #) + (:NEWLINE) + )|#) + +;;; Figuring out how the navigation into nested structures works + +#| + +In the inspectors' buffer RET is bound to +slime-inspector-operate-on-point, here's its docstring: + +Invoke the command for the text at point. +1. If point is on a value then recursivly call the inspector on +that value. +2. If point is on an action then call that action. +3. If point is on a range-button fetch and insert the range. + +Internally, it uses (slime-inspector-property-at-point) to know what's +under the point. In turns, this return the first property found +between slime-part-number, slime-range-button and slime-action-number. + +slime-inspector-operate-on-point calls swank:inspect-nth-part to +inspect other values + +|# + +#++ +(progn + (istate.parts *istate*) + #( + ;; # + 2 + 7 + EQL + 1.5 + 1.0 + :A + "asdf" + :B + (A (NESTED (LIST #(WITH-AN-ARRAY 42)))))) + +#++ +(trace + swank:init-inspector + swank::reset-inspector + swank:inspector-eval + swank:inspect-nth-part + swank::inspect-object + swank:emacs-inspect + swank:inspector-reinspect) + + +;;; First of all: we lose the original form in init-inspector, +;;; inspector-eval, inspect-in-frame (which uses eval-in-frame) + +(defparameter *place* nil) + +;; changes: save the initial form into *place* +(defslimefun init-inspector (string) + (with-buffer-syntax () + (with-retry-restart (:msg "Retry SLIME inspection request.") + (reset-inspector) + (let ((form (read-from-string string))) + (setf *place* (list form)) + (inspect-object (eval form)))))) + + + +;;; Now, when swank:inspect-nth-part is called, how do we know if the +;;; part is a place "inside" the object being inpected? Because there +;;; are many "parts" which are just metadata of the object (like its +;;; type, length, etc.) + +;; changes: add place; save a cons into PARTS instead of OBJECT +(defun value-part (object string place parts) + (list :value + (or string (print-part-to-string object)) + (assign-index (cons object place) + parts))) + +;; changes: add &optional place +(defun iline (label value &optional place) + `(:line ,label ,value ,place)) + +;; changes: add &key place +(defun label-value-line (label value &key (newline t) place) + "Create a control list which prints \"LABEL: VALUE\" in the inspector. +If NEWLINE is non-NIL a `(:newline)' is added to the result." + (list* (princ-to-string label) ": " `(:value ,value nil ,place) + (if newline '((:newline)) nil))) + +;; changes: add &optional place +(defmacro label-value-line* (&rest label-values) + ` (append ,@(loop for label-value in label-values + collect (destructuring-bind (label value &optional place) + label-value + `(label-value-line ,label ,value :place ,place))))) + +;; changes: add &optional place to :value and non-optional to :line +(defun prepare-part (part istate) + (let ((newline '#.(string #\newline))) + (etypecase part + (string (list part)) + (cons (dcase part + ((:newline) (list newline)) + ;; added "place" + ((:value obj &optional str place) + (list (value-part obj str place (istate.parts istate)))) + ((:label &rest strs) + (list (list :label (apply #'cat (mapcar #'string strs))))) + ((:action label lambda &key (refreshp t)) + (list (action-part label lambda refreshp + (istate.actions istate)))) + ((:line label value place) + (list (princ-to-string label) ": " + (value-part value nil place (istate.parts istate)) + newline))))))) + +;; these migth not be up-to-date +#++ +(flet ((test-prepare-part (object part) + (let* ((istate (make-istate :object object :previous nil + :verbose *inspector-verbose*))) + (prepare-part part istate)))) + (values + (test-prepare-part 'x `(:value x)) + (test-prepare-part 'x `(:value x "X")) + (test-prepare-part 'x `(:value x nil (gethash *))))) + +;; changes: handle cons instead of objects directly +(defslimefun inspect-nth-part (index) + (with-buffer-syntax () + (destructuring-bind (object . place) + (inspector-nth-part index) + (push place *place*) + (inspect-object object)))) + +;; changes: keep *place* in sync +(defslimefun inspector-pop () + "Inspect the previous object. +Return nil if there's no previous object." + (with-buffer-syntax () + (cond ((istate.previous *istate*) + (pop *place*) + (setq *istate* (istate.previous *istate*)) + (istate>elisp *istate*)) + (t nil)))) + + + +(defun place () + (car *place*)) + +;; new around method: prepend the "place" +(defmethod emacs-inspect :around (o) + (let ((parts (call-next-method)) + (line (iline "Place" (place)))) + (if (listp parts) + `(,line ,@parts) + (lcons line parts)))) + + + +;; changes: add "car" and "cons" places +(defun inspect-cons (cons) + (label-value-line* + ('car (car cons) `(car ,(place))) + ('cdr (cdr cons) `(cdr ,(place))))) + +(defun inspect-list-aux (list) + (loop for i from 0 + for rest on list + while (consp rest) + append + (if (listp (cdr rest)) + (label-value-line i + (car rest) + :place `(nth ,i ,(place))) + (label-value-line* (i (car rest)) + (:tail (cdr rest) `(cdr (last ,(place)))))))) + + +;; changes: add places to values +(defmethod emacs-inspect ((ht hash-table)) + (append + (label-value-line* + ("Count" (hash-table-count ht)) + ("Size" (hash-table-size ht)) + ("Test" (hash-table-test ht)) + ("Rehash size" (hash-table-rehash-size ht)) + ("Rehash threshold" (hash-table-rehash-threshold ht))) + (let ((weakness (hash-table-weakness ht))) + (when weakness + (label-value-line "Weakness:" weakness))) + (unless (zerop (hash-table-count ht)) + `((:action "[clear hashtable]" + ,(lambda () (clrhash ht))) (:newline) + "Contents: " (:newline))) + (let ((content (hash-table-to-alist ht))) + (cond ((every (lambda (x) (typep (first x) '(or string symbol))) content) + (setf content (sort content 'string< :key #'first))) + ((every (lambda (x) (typep (first x) 'real)) content) + (setf content (sort content '< :key #'first)))) + (loop for (key . value) in content appending + `((:value ,key) " = " (:value ,value nil + ;; Added this: + (gethash ,key ,(place))) + " " (:action "[remove entry]" + ,(let ((key key)) + (lambda () (remhash key ht)))) + (:newline)))))) + +;; changes: add "row-major-aref" place +(defun emacs-inspect-array-aux (array) + (unless (= 0 (array-total-size array)) + (lcons* + "Contents:" '(:newline) + (labels ((k (i max) + (cond ((= i max) '()) + (t (lcons (iline i (row-major-aref array i) `(row-major-aref ,(place) ,i)) + (k (1+ i) max)))))) + (k 0 (array-total-size array)))))) + +;; changes: add "row-major-aref" place +(defun emacs-inspect-vector-with-fill-pointer-aux (array) + (let ((active-elements? (< 0 (fill-pointer array))) + (inactive-elements? (< (fill-pointer array) + (array-total-size array)))) + (labels ((k (i max cont) + (cond ((= i max) (funcall cont)) + (t (lcons (iline i (row-major-aref array i) `(row-major-aref ,(place) ,i)) + (k (1+ i) max cont))))) + (collect-active () + (if active-elements? + (lcons* + "Active elements:" '(:newline) + (k 0 (fill-pointer array) + (lambda () (collect-inactive)))) + (collect-inactive))) + (collect-inactive () + (if inactive-elements? + (lcons* + "Inactive elements:" '(:newline) + (k (fill-pointer array) + (array-total-size array) + (constantly '()))) + '()))) + (collect-active)))) + + +;;; Extra notes + +#| + +When re-inspecting, the original *package* is lost, meaning that the +symbols that were not printed with a package qulifier might have one +after re-inspecting, even though we're inspecting the exact same +object(s). + +It would be nice to be able to have multiple inspectors at the same +time. + +It would be nice to easily re-evaluate the original form. Perhaps even +re-read it! + +This feature would be better implemented either directly in swank, or +as it's own inspector (there is a "fancy inspector" contrib...) + +This feature would go well with the "egraph refactoring" feature... + +Because the inspector has "place" form, it could be easier to edit +stuff in the inspector... + +|# diff --git a/scratch-files/notes/lint.lisp b/scratch-files/notes/lint.lisp index 72ad57d2..4d12160a 100644 --- a/scratch-files/notes/lint.lisp +++ b/scratch-files/notes/lint.lisp @@ -1,3 +1,5 @@ +;; For examples of equivalent forms, search for the string "==" in +;; ~/quicklisp/dists/quicklisp/software/clhs-0.6.3/HyperSpec-7-0/HyperSpec/ ;; definitely don't need the "if" ;; probably don't need the "null" @@ -77,6 +79,32 @@ inline-function ;; should be (member x '(1 2 3) :test #'=) +(eql nil x) +;; same with equal or equalp, but not eq + +(typep 'x nil) +;; you probably want +(typep 'x 'null) +;; same with check-type, declarations, types in struct and classes, etc. + +keywordp + +(typep x some-type-that-has-an-existing-predictate) +;; use the predicate instead +symbol keyword integer number string cons atom list + +(typep 0 '(integer 1)) +(plusp 0) + + +;; No need to nest variadic functions or macros +(and (and a b) c) => (and a b c) +(or (or a b) c) => (or a b c) +(+ (+ a b) c) => (+ a b c) +(* (* a b) c) => (* a b c) +(min (min a b) c) => (min a b c) +(max (max a b) c) => (max a b c) + ;; nested car and cdr can be replaced by c[ad]+r functions @@ -292,3 +320,86 @@ prog* ;; it's redundent to add a quote before t, nil, or any keywords ':ok 'nil 'y + +Bad: +(defmethod ((x '(:eql y)))) +(defmethod ((x (:eql y)))) +(defmethod ((x (:eql 'y)))) +(defmethod ((x '(:eql 'y)))) +(defmethod ((x 'y))) +(defmethod ((x :y))) +Good: +(defmethod (x (eql 'y))) +(defmethod (x (eql :y))) + + + +(defun f (y) + ;; Copy-pasted code, where the variables doesn't match, breeze + ;; should be able to "quickfix" this relpacing either x or y by the + ;; other. + (if (plusp x) + x + (- x))) + + +;; Would be nice if we could detect this kind of typos... +(defu nasdf (...) ...) + + +Very bad: +char\= + +Good: +char/= + + +(assoc "some string" alist) +=> +(assoc "some string" alist :test 'equal) + + +(return-from 'x) +=> +(return-from x) + + +(cons x nil) === (list x) + + +(not (= ...)) +=> +(/= ...) + + +;; check for unused import-from and import + +;; nested defun +;; defvar, defparameterm, defconstant inside defun +;; warn about non-toplevel defparameter, defvar, defconstant, defmacro ? +;; defparameter or defvar without earmuffs +;; defconstant without +...+ + + + +(let ((b (f1 a))) + (and b (let ((c (f2 b))) + (and c (f3 c))))) + +=> + +(let ((b (f1 a)) + (c (and b (f2 b)))) + (and c (f3 c))) + +=> + +(when-let* ((b (f1 a)) + (c (and b (f2 b)))) + (f3 c)) + + + +;; Warn that the body is empty +(let ((x 32))) +;; Same with flet, macrolet, symbol-macrolet, etc diff --git a/scratch-files/notes/strutural-editing.lisp b/scratch-files/notes/strutural-editing.lisp index a0672495..4eb737de 100644 --- a/scratch-files/notes/strutural-editing.lisp +++ b/scratch-files/notes/strutural-editing.lisp @@ -1,8 +1,293 @@ -;; || represents the cursor +;;; -*- mode: erts; -*- -(progn - ;; some comment - ||(+ 2 2)) +Point-Char: $ ;; with paredit, you can't use paredit-splice-sexp-killing-backward ;; (M-up) without losing the comment +Name: splice backward keeps comments +Command: breeze:splice-sexp-backwards +;; TODO automatically skip if the "command" doesn't exist +Skip: t + +=-= +(progn + ;; some comment + $(+ 2 2)) +=-= +;; some comment +$(+ 2 2) +=-=-= + + +Name: contextual split sexp +Command: breeze:split-sexp + +=-= +(:export + #:a + #:b$ + #:c + #:d) +=-= +(:export + #:a + #:b) +(:export + #:c + #:d) +=-=-= + +Name: fill-paragraph in multiline-comments +Command: breeze:fill-paragraph + +=-= +#| this is a long line this is a long line this is a long line this is a long line |# +=-= +#| this is a long line this is a long line this is a long line this is +a long line |# +=-=-= + +; The current behaviour of emacs is completely wrong: +; =-= +; #| this is a long line this is a long line this is a long line this is a long line |# +; =-= +; #| this is a long line this is a long line this is a long line this is +; #| a long line |# +; =-=-= + +; =-= +; #| this is a long line this is a long line this is a long line this is a long line |# +; #| this is a long line this is a long line this is a long line this is a long line |# +; =-= +; #| this is a long line this is a long line this is a long line this is +; #| a long line |# this is a long line this is a long line this is a +; #| long line this is a long line |# +; =-=-= + + +;; TODO: +;; C- and C- don't work inside comments (M- and +;; M- do what I instinctively think C- and C- +;; should do). + +;; TODO: +;; M-" doesn't warp the next expression in quotes inside comments. + + +Name: delete-char should let you delete a character to fix syntax errors +Command: breeze:delete-char + +=-= +(if $; test) +=-= +(if $ test) +=-=-= + +Name: delete-forward-char should let you delete a character to fix syntax errors +Command: breeze:delete-forward-char + +=-= +(if ;$ test) +=-= +(if $ test) +=-=-= + + +Name: forward-slurp-sexp should not ignore comments +Command: breeze:forward-slurp-sexp + +=-= +($) ;; asdf +=-= +($ ;; asdf + ) +=-=-= + +=-= +($) #| asdf |# +=-= +($ #| asdf |#) +=-=-= + +=-= +($) +#| asdf |# +=-= +($ + #| asdf |#) +=-=-= + +Name: quickfix #:#: + +=-= +#:#:x +=-= +#:x +=-=-= + + +Name: quickfix spaces between closing parens + +=-= +(() ) +=-= +(()) +=-=-= + + +=-= +(( + ) ) +=-= +(( + )) +=-=-= + + +=-= +(( + ) +) +=-= +(( + )) +=-=-= + + +Name: forward-barf-sexp and comments + +=-= +(;; 1 + a$ + ;; 2 + b) +=-= +(;; 1 + a) +;; 2 +b +=-=-= + + +;; With emacs, you can't use kill-sexp (C-M-k) to kill a comment +Name: kill-sexp-comments + +=-= +($;; 1 + a) +=-= +($a) +=-=-= + +=-= +($;; 1 + ;; 2 + ;; 3 + a) +=-= +($a) +=-=-= + +=-= +($ +#| +some block comment +|# + a) +=-= +($a) +=-=-= + +;; With emacs, kill-sexp (C-M-k) doesn't parse some things correctly +Name: kill-sexp-multiple-tokens-without-spaces + +=-= +$abc,def +=-= +$,def +=-=-= +;; emacs would delete the whole abc,def + + +;; Like paredit-kill, which is like kill-line, but keeping the +;; structure valid +Name: kill + +=-= +(a b)$ ; some comment +=-= +(a b)$ +=-=-= + +=-= +($a b) ; some comment +=-= +($) ; some comment +=-=-= + +=-= +$(a b) ; some comment +=-= +$ +=-=-= + +=-= +(a "b $c d") +=-= +(a "b $") +=-=-= + + +;; paredit-backslash is annoying +Name: backslash + +;; It should _not_ read a character if just inserting the backslash +;; would resuld in valid code. +=-= +"$n" +=-= +"\n" +=-=-= + +;; When deleting an escaped character, it should not delete the +;; backslash along with it, it should intead try to read a new +;; character to escape... probably + +Code: breeze-backward-delete + +=-= +\n$ +=-= +\$ +=-=-= + +;; It would be nice if "backslash" would insert the char-name of the +;; character when it make sense + +Code: \ SPC + +=-= +$ +=-= +\Space +=-=-= + +;; paredit seems to have a silly bug, if you type \, it will ask for a +;; character to escape and if you type \, it will ask for a character +;; to escape and if you type \, it will ask for a character to escape +;; and if you type \, it will ask for a character to escape and if you +;; type \, it will ask for a character to escape and if you type \, + +Name: doublequote + +;; I don't like that paredit does "forward-char" when I try to enter a +;; double quote at the end of a string. Which happens all the time +;; when I write docstrings. + +Code: breeze-doublequote + +=-= +"$" +=-= +"\"" +=-=-= diff --git a/scratch-files/snippets.lisp b/scratch-files/snippets.lisp index 98fb6c56..a8226957 100644 --- a/scratch-files/snippets.lisp +++ b/scratch-files/snippets.lisp @@ -60,7 +60,7 @@ (defun indent (&optional (stream *standard-output*)) "Print a number of space based on the value of *indentation*." - (princ (str:repeat *indentation* " ") stream)) + (princ (breeze.utils:repeat-string *indentation* " ") stream)) (defun transform-snippet (body) "Take a skeleton and generate the code to execute it." diff --git a/scratch-files/structural-editing.el b/scratch-files/structural-editing.el new file mode 100644 index 00000000..9b9bf140 --- /dev/null +++ b/scratch-files/structural-editing.el @@ -0,0 +1,598 @@ + + +(defun breeze--insert-interactive-commands (prefix) + "Insert list of commands that starts with PREFIX." + (loop for x being the symbols + if (and (commandp x) + (or (string-prefix-p prefix (symbol-name x)))) + do (insert (symbol-name x) "\n"))) + +(defun breeze--insert-describe-keymap (keymap) + "Insert the description of KEYMAP" + (describe-keymap keymap) + (insert + (mapconcat + (lambda (line) + (concat ";; " line "\n")) + (split-string + (with-current-buffer "*Help*" + (buffer-substring-no-properties (point-min) (point-max))) + "\n")))) + + +;;; Paredit + +(breeze--insert-interactive-commands "paredit-") + +paredit-backward-up +paredit-backward-down +paredit-comment-dwim +paredit-add-to-next-list +paredit-add-to-previous-list +paredit-close-parenthesis-and-newline +paredit-wrap-square +paredit-split-sexp +paredit-RET +paredit-semicolon +paredit-C-j +paredit-wrap-sexp +paredit-open-square +paredit-hack-kill-region +paredit-meta-doublequote-and-newline +paredit-convolute-sexp +paredit-close-angled +paredit-close-round-and-newline +paredit-newline +paredit-meta-doublequote +paredit-close-bracket-and-newline +paredit-forward-down +paredit-backslash +paredit-close-round +paredit-kill-region +paredit-wrap-curly +paredit-copy-as-kill +paredit-recentre-on-sexp +paredit-join-with-next-list +paredit-close-square +paredit-open-curly +paredit-delete-region +paredit-join-with-previous-list +paredit-backward-kill-word +paredit-backward-delete +paredit-wrap-angled +paredit-reindent-defun +paredit-recenter-on-defun +paredit-open-angled +paredit-yank-pop +paredit-backward-slurp-sexp +paredit-splice-sexp +paredit-backward +paredit-forward-up +paredit-forward +paredit-close-bracket +paredit-focus-on-defun +paredit-close-parenthesis +paredit-open-parenthesis +paredit-insert-html-examples +paredit-open-bracket +paredit-wrap-round +paredit-close-angled-and-newline +paredit-open-round +paredit-delete-char +paredit-forward-barf-sexp +paredit-splice-sexp-killing-backward +paredit-join-sexps +paredit-close-curly +paredit-close-square-and-newline +paredit-forward-slurp-sexp +paredit-mode +paredit-splice-sexp-killing-forward +paredit-forward-delete +paredit-backward-barf-sexp +paredit-raise-sexp +paredit-kill +paredit-forward-kill-word +paredit-recenter-on-sexp +paredit-close-curly-and-newline +paredit-doublequote + + +(breeze--insert-describe-keymap paredit-mode-map) +;; +;; Key Binding +;; +;; C-d paredit-delete-char +;; C-j paredit-C-j +;; C-k paredit-kill +;; RET paredit-RET +;; " paredit-doublequote +;; ( paredit-open-round +;; ) paredit-close-round +;; ; paredit-semicolon +;; [ paredit-open-square +;; \ paredit-backslash +;; ] paredit-close-square +;; DEL paredit-backward-delete +;; C-( paredit-backward-slurp-sexp +;; C-) paredit-forward-slurp-sexp +;; C-{ paredit-backward-barf-sexp +;; C-} paredit-forward-barf-sexp +;; C-M- paredit-backward-slurp-sexp +;; C-M- paredit-backward-barf-sexp +;; C- paredit-forward-barf-sexp +;; C- paredit-forward-slurp-sexp +;; M- paredit-splice-sexp-killing-forward +;; M- paredit-splice-sexp-killing-backward +;; paredit-forward-delete +;; paredit-forward-delete +;; +;; C-M-b paredit-backward +;; C-M-d paredit-forward-down +;; C-M-f paredit-forward +;; C-M-n paredit-forward-up +;; C-M-p paredit-backward-down +;; C-M-u paredit-backward-up +;; M-" paredit-meta-doublequote +;; M-( paredit-wrap-round +;; M-) paredit-close-round-and-newline +;; M-; paredit-comment-dwim +;; M-? paredit-convolute-sexp +;; M-J paredit-join-sexps +;; M-S paredit-split-sexp +;; M-d paredit-forward-kill-word +;; M-q paredit-reindent-defun +;; M-r paredit-raise-sexp +;; M-s paredit-splice-sexp +;; M-DEL paredit-backward-kill-word +;; ESC C- paredit-backward-slurp-sexp +;; ESC C- paredit-backward-barf-sexp +;; ESC paredit-splice-sexp-killing-forward +;; ESC paredit-splice-sexp-killing-backward +;; +;; C-c C-M-l paredit-recenter-on-sexp +;; +;; [back] +;; + + + +;;; Lispy + +(breeze--insert-interactive-commands "lispy-") + +lispy-brackets-barf-to-point-or-jump-nostring +lispy-tab +lispy-left +lispy-parens-auto-wrap +lispy-underscore +lispy-join +lispy-build-semanticdb +lispy-backward +lispy-extract-block +lispy-x +lispy-mark-car +lispy-move-end-of-line +lispy-goto-def-down +lispy-eval +lispy-show-top-level +lispy-goto-local +lispy-move-outline-up +lispy-to-ifs +lispy-flatten +lispy-goto-elisp-commands +lispy-raise-sexp +lispy-newline-and-indent +lispy-goto-mode +lispy-expr-canonical-p +lispy-backward-slurp-sexp +lispy-mark +lispy-back +lispy-goto-projectile +lispy-barf +lispy-stringify-oneline +lispy-delete +lispy-arglist-inline +lispy-brackets-auto-wrap +lispy-ace-symbol +lispy-raise +lispy-comment +lispy-mode +lispy-mark-right +lispy-right +lispy-outline-right +lispy-slurp-or-barf-left +lispy-close-square +lispy-doublequote +lispy-fill +lispy-cursor-ace +lispy-parens-down +lispy-convolute-left +lispy-newline-and-indent-plain +lispy-close-curly +lispy-convolute-sexp +lispy-goto-recursive +lispy-brackets +lispy-down-slurp +lispy-eval-current-outline +lispy-shifttab +lispy-toggle-thread-last +lispy-move-up +lispy-reverse +lispy-out-forward-newline +lispy-splice-sexp-killing-forward +lispy-unstringify +lispy-ace-symbol-replace +lispy-cd +lispy-follow +lispy-cleanup +lispy-to-defun +lispy-forward-barf-sexp +lispy-stringify +lispy-ediff-regions +lispy-edebug +lispy-up +lispy-move-beginning-of-line +lispy-meta-return +lispy-widen +lispy-open-line +lispy-view-test +lispy-right-nostring +lispy-kill-word +lispy-unbind-variable-clojure +lispy-different +lispy-tick +lispy-outline-promote +lispy-ace-subword +lispy-oneline +lispy-left-maybe +lispy-narrow +lispy-forward-delete +lispy-braces-auto-wrap +lispy-outline-prev +lispy-outline-next +lispy-delete-backward-or-splice-or-slurp +lispy-mark-list +lispy-debug-step-in +lispy-edebug-stop +lispy-forward-slurp-sexp +lispy-store-region-and-buffer +lispy-quotes +lispy-tilde +lispy-wrap-round +lispy-eval-and-replace +lispy-wrap-brackets +lispy-delete-backward +lispy-iedit +lispy-braces-barf-to-point-or-jump-nostring +lispy-hash +lispy-ace-char +lispy-move-right +lispy-other-verb +lispy-mark-left +lispy-forward +lispy-alt-multiline +lispy-teleport +lispy-backward-barf-sexp +lispy-indent-adjust-parens +lispy-parens +lispy-outline-goto-child +lispy-alt-line +lispy-multiline +lispy-close-round-and-newline +lispy-other-space +lispy-quit +lispy-map-done +lispy-comment-region +lispy-down +lispy-cursor-down +lispy-knight-up +lispy-flow +lispy-describe-inline +lispy-colon +lispy-clone +lispy-splice-sexp-killing-backward +lispy-string-oneline +lispy-dedent-adjust-parens +lispy-hat +lispy-barf-to-point-nostring +lispy-backtick +lispy-open-curly +lispy-to-cond +lispy-goto-symbol +lispy-visit +lispy-open-square +lispy-at +lispy-backward-delete +lispy-x-more-verbosity +lispy-kill-sentence +lispy-insert-outline-below +lispy-bind-variable +lispy-buffer-kill-ring-save +lispy-outline-demote +lispy-meta-doublequote +lispy-outline-left +lispy-other-mode +lispy-to-lambda +lispy-new-copy +lispy-wrap-braces +lispy-slurp +lispy-let-flatten +lispy-goto +lispy-split +lispy-space +lispy-eval-expression +lispy-describe +lispy-goto-verb +lispy-view +lispy-kill +lispy-undo +lispy-delete-or-splice-or-slurp +lispy-yank +lispy-ace-paren +lispy-barf-to-point +lispy-convolute +lispy-repeat +lispy-slurp-or-barf-right +lispy-up-slurp +lispy-eval-and-insert +lispy-kill-at-point +lispy-braces +lispy-ert +lispy-move-down +lispy-backward-kill-word +lispy-paste +lispy-move-left +lispy-eval-other-window +lispy-knight-down +lispy-eval-and-comment +lispy-goto-def-ace +lispy-parens-barf-to-point-or-jump-nostring +lispy-extract-defun +lispy-insert-outline-left +lispy-mark-symbol +lispy-describe-bindings-C-4 +lispy-unbind-variable +lispy-raise-some +lispy-splice +lispy--ediff-regions +lispy-beginning-of-defun + + +(breeze--insert-describe-keymap lispy-mode-map) +;; +;; Key Binding +;; +;; C-a lispy-move-beginning-of-line +;; C-d lispy-delete +;; C-e lispy-move-end-of-line +;; C-j lispy-newline-and-indent +;; C-k lispy-kill +;; RET lispy-newline-and-indent-plain +;; C-y lispy-yank +;; SPC lispy-space +;; " lispy-quotes +;; # lispy-hash +;; ' lispy-tick +;; ( lispy-parens +;; ) lispy-right-nostring +;; + special-lispy-join +;; - special-lispy-ace-subword +;; . special-lispy-repeat +;; / special-lispy-splice +;; 0 .. 9 special-digit-argument +;; : lispy-colon +;; ; lispy-comment +;; < special-lispy-barf +;; > special-lispy-slurp +;; @ lispy-at +;; A special-lispy-beginning-of-defun +;; B special-lispy-ediff-regions +;; C special-lispy-convolute +;; D special-pop-tag-mark +;; E special-lispy-eval-and-insert +;; F special-lispy-follow +;; G special-lispy-goto +;; H special-lispy-ace-symbol-replace +;; I special-lispy-shifttab +;; J special-lispy-outline-next +;; K special-lispy-outline-prev +;; L special-lispy-outline-goto-child +;; M special-lispy-alt-multiline +;; N special-lispy-narrow +;; O special-lispy-oneline +;; P special-lispy-paste +;; Q special-lispy-ace-char +;; R special-lispy-raise-some +;; S special-lispy-stringify +;; V special-lispy-visit +;; W special-lispy-widen +;; X special-lispy-convolute-left +;; Z special-lispy-edebug-stop +;; [ lispy-backward +;; ] lispy-forward +;; ^ lispy-hat +;; _ special-lispy-underscore +;; ` lispy-backtick +;; a special-lispy-ace-symbol +;; b special-lispy-back +;; c special-lispy-clone +;; d special-lispy-different +;; e special-lispy-eval +;; f special-lispy-flow +;; g special-lispy-goto-local +;; h special-lispy-left +;; i special-lispy-tab +;; j special-lispy-down +;; k special-lispy-up +;; l special-lispy-right +;; m special-lispy-mark-list +;; n special-lispy-new-copy +;; o special-lispy-other-mode +;; p special-lispy-eval-other-window +;; q special-lispy-ace-paren +;; r special-lispy-raise +;; s special-lispy-move-down +;; t special-lispy-teleport +;; u special-lispy-undo +;; v special-lispy-view +;; w special-lispy-move-up +;; x special-lispy-x +;; y special-lispy-occur +;; z special-lh-knight/body +;; { lispy-braces +;; } lispy-brackets +;; ~ special-lispy-tilde +;; DEL lispy-delete-backward +;; C-, lispy-kill-at-point +;; C-1 lispy-describe-inline +;; C-2 lispy-arglist-inline +;; C-3 lispy-right +;; C-4 lispy-x +;; C-7 lispy-cursor-down +;; C-8 lispy-parens-down +;; C-9 lispy-out-forward-newline +;; C- lispy-open-line +;; M- lispy-outline-demote +;; M- lispy-meta-return +;; M- lispy-outline-promote +;; lispy-shifttab +;; +;; M-RET lispy-meta-return +;; M-, pop-tag-mark +;; M-. lispy-goto-symbol +;; M-J lispy-join +;; M-d lispy-kill-word +;; M-i lispy-iedit +;; M-j lispy-split +;; M-k lispy-kill-sentence +;; M-m lispy-mark-symbol +;; M-o lispy-left-maybe +;; M-q lispy-fill +;; M-DEL lispy-backward-kill-word +;; C-M-, lispy-mark +;; +;; [back] +;; + + + +;;; Smartparens + +(use-package smartparens) + +(breeze--insert-interactive-commands "smartparens-") + +smartparens-mode +smartparens-global-mode +smartparens-global-strict-mode +smartparens-strict-mode + +(breeze--insert-interactive-commands "sp-") + +sp-convolute-sexp +sp-splice-sexp +sp-splice-sexp-killing-backward +sp-prefix-symbol-object +sp-use-paredit-bindings +sp-newline +sp-use-smartparens-bindings +sp-end-of-previous-sexp +sp-backward-whitespace +sp-select-next-thing +sp-kill-region +sp-delete-char +sp-absorb-sexp +sp-forward-sexp +sp-slurp-hybrid-sexp +sp-beginning-of-next-sexp +sp-select-next-thing-exchange +sp-beginning-of-previous-sexp +sp-delete-region +sp-backward-sexp +sp-backward-delete-symbol +sp-kill-whole-line +sp-up-sexp +sp-emit-sexp +sp-prefix-save-excursion +sp-end-of-next-sexp +sp-kill-word +sp-copy-sexp +sp-raise-sexp +sp-previous-sexp +sp-cheat-sheet +sp-join-sexp +sp-backward-barf-sexp +sp-transpose-hybrid-sexp +sp-forward-slurp-sexp +sp-clone-sexp +sp-select-previous-thing +sp-backward-delete-word +sp-backward-copy-sexp +sp-remove-active-pair-overlay +sp-kill-sexp +sp-indent-adjust-sexp +sp-splice-sexp-killing-forward +sp--kill-or-copy-region +sp-indent-defun +sp-wrap-cancel +sp-change-inner +sp-prefix-pair-object +sp-backward-slurp-sexp +sp-backward-kill-word +sp-prefix-tag-object +sp-delete-word +sp-comment +sp-backward-unwrap-sexp +sp-backward-delete-char +sp-show-enclosing-pair +sp-add-to-previous-sexp +sp-rewrap-sexp +sp-narrow-to-sexp +sp-backward-up-sexp +sp-extract-before-sexp +sp-select-previous-thing-exchange +sp-backward-parallel-sexp +sp-skip-forward-to-symbol +sp-backward-symbol +sp-add-to-next-sexp +sp-skip-backward-to-symbol +sp-forward-barf-sexp +sp-dedent-adjust-sexp +sp-delete-symbol +sp-describe-system +sp-splice-sexp-killing-around +sp-forward-whitespace +sp-down-sexp +sp-backward-kill-symbol +sp-push-hybrid-sexp +sp-extract-after-sexp +sp-transpose-sexp +sp-end-of-sexp +sp-unwrap-sexp +sp-backward-down-sexp +sp-kill-hybrid-sexp +sp-beginning-of-sexp +sp-forward-symbol +sp-forward-parallel-sexp +sp-kill-symbol +sp-next-sexp +sp-highlight-current-sexp +sp-split-sexp +sp-backward-kill-sexp +sp-swap-enclosing-sexp +sp-mark-sexp + +(breeze--insert-describe-keymap smartparens-mode-map) +;; ok... smartparens is being a smartass about it's keymap, it is +;; defined dynamically (because it supports many languages, and +;; multiple "styles"), see: +;; +;; - sp-lisp-modes +;; - sp--populate-keymap +;; - sp-use-paredit-bindings +;; - sp-use-smartparens-bindings +;; - sp--set-base-key-bindings + + +;; See https://ebzzry.com/en/emacs-pairs/ +;; and https://github.com/Fuco1/smartparens/wiki diff --git a/scratch-files/try-fresnel.lisp b/scratch-files/try-fresnel.lisp new file mode 100644 index 00000000..2464bc14 --- /dev/null +++ b/scratch-files/try-fresnel.lisp @@ -0,0 +1,22 @@ +(cl:in-package #:cl-user) + +(defpackage #:breeze.try-fresnel + (:documentation "Trying out the \"bidirectional transform\" library \"fresnel\".") + (:use #:cl) + (:local-nicknames (:l :fresnel/lens))) + +(in-package #:breeze.try-fresnel) + +(ql:quickload 'fresnel) + + +(fresnel/lens: + + ) + +(let ((lens (l:make-lens #'-)) + (x 10)) + (list + (l:backward + lens + (l:forward lens x)))) diff --git a/scripts/demo/demo-recorder.sh b/scripts/demo/demo-recorder.sh index dda9b8d9..e9d5e0ff 100755 --- a/scripts/demo/demo-recorder.sh +++ b/scripts/demo/demo-recorder.sh @@ -84,6 +84,7 @@ function emacs_tty() { } function wait_for_emacs_to_stop() { + # TODO Time out? while pgrep emacs do sleep 1 diff --git a/scripts/doc.sh b/scripts/doc.sh index 2a98cf06..cc91ea22 100755 --- a/scripts/doc.sh +++ b/scripts/doc.sh @@ -3,6 +3,16 @@ # This script is used to generate the documentation # +set -e + cd "$(git rev-parse --show-toplevel)" -sbcl --non-interactive --eval '(ql:quickload :breeze)' --eval '(breeze.documentation::generate-documentation)' +mkdir -p public/ + +sbcl --noinform --non-interactive \ + --eval "(declaim (optimize (debug 3) (speed 0) (safety 3)))" \ + --eval "(asdf:load-asd (truename \"breeze.asd\"))" \ + --eval "(ql:quickload '#:breeze/doc)" \ + --eval '(breeze.documentation::generate-documentation)' + +cp docs/style.css public/ diff --git a/scripts/load-dependencies.lisp b/scripts/load-dependencies.lisp new file mode 100644 index 00000000..f7d8fc78 --- /dev/null +++ b/scripts/load-dependencies.lisp @@ -0,0 +1,35 @@ +(cl:in-package #:cl-user) + +(require '#:asdf) + +#-quicklisp +(let ((quicklisp-init #P"/opt/quicklisp/setup.lisp")) + (when (probe-file quicklisp-init) + (load quicklisp-init))) + +(asdf:load-asd + (merge-pathnames "../breeze.asd" *load-truename*)) + +(flet ((find-all-related-systems (system) + "Given a system, find all systems defined in the same system definition +file (including the one passed as argument)." + (let ((result ()) + (asd-pathname (asdf:system-source-file system))) + (asdf:map-systems (lambda (system) + ;; TODO Perhaps use asdf:primary-system-name + (when (equal asd-pathname + (asdf:system-source-file system)) + (push system result)))) + result))) + (let* ((systems (find-all-related-systems "breeze")) + (dependencies (remove-if (lambda (system-name) + (uiop:string-prefix-p "breeze" system-name)) + (loop + :for system-name :in systems + :for system = (asdf:find-system system-name) + :for dependecy-list = (asdf:system-depends-on system) + :append (copy-list dependecy-list))))) + (ql:quickload dependencies) + (mapcar #'asdf:register-immutable-system dependencies))) + +(uiop:dump-image "dependencies.core") diff --git a/scripts/org-publish-project.el b/scripts/org-publish-project.el new file mode 100644 index 00000000..5361cb7c --- /dev/null +++ b/scripts/org-publish-project.el @@ -0,0 +1,44 @@ + +(message "Installing packages (from ELPA)") + +(package-initialize) +(setf package-selected-packages + '(htmlize)) +(package-install-selected-packages t) + + +(message "Publishing...") + +(require 'org) +(require 'org-id) +(require 'htmlize) + +;; See (describe-variable 'org-publish-project-alist) +;; See https://orgmode.org/manual/Publishing-options.html for more options +(let* ((forcep t) ; "forcep" is for interactive sessions. + (org-id-link-to-org-use-id t) + (default-directory + (expand-file-name + (concat + (file-name-directory (or load-file-name buffer-file-name)) + ".."))) + (root "docs") + (project-alist + `("breeze" + :base-directory ,root + :publishing-function org-html-publish-to-html + :publishing-directory "./public" + + :author "Francis St-Amour" + :creator "Francis St-Amour" + :with-author nil + :html-validation-link nil + + :auto-sitemap t + ;; :makeindex t + :with-toc nil + ))) + (org-id-update-id-locations (directory-files root t "\\.org$")) + (org-publish project-alist forcep) + (copy-file "docs/listing-breeze.html" "public/") + (copy-file "docs/style.css" "public/")) diff --git a/scripts/publish-doc.sh b/scripts/publish-doc.sh deleted file mode 100755 index d600b77c..00000000 --- a/scripts/publish-doc.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# -# Script to upload documentation to s3, effectively publishing it. -# -# nix-shell -p awscli - -set -e - -if [ "$(git rev-parse --abbrev-ref HEAD)" == "master" ]; then - target="" -else - target="dev/" -fi - - -cd "$(git rev-parse --show-toplevel)/docs" - -files="index.html \ -style.css" - -for file in $files; do - echo "Checking if \"$file\" exists..." - test -f $file -done - -for file in $files; do - aws s3 cp --acl public-read $file s3://www.fstamour.com/breeze/$target -done diff --git a/scripts/quicklisp.lisp b/scripts/quicklisp.lisp new file mode 100644 index 00000000..6cda4724 --- /dev/null +++ b/scripts/quicklisp.lisp @@ -0,0 +1,1757 @@ +;;;; +;;;; This is quicklisp.lisp, the quickstart file for Quicklisp. To use +;;;; it, start Lisp, then (load "quicklisp.lisp") +;;;; +;;;; Quicklisp is beta software and comes with no warranty of any kind. +;;;; +;;;; For more information about the Quicklisp beta, see: +;;;; +;;;; http://www.quicklisp.org/beta/ +;;;; +;;;; If you have any questions or comments about Quicklisp, please +;;;; contact: +;;;; +;;;; Zach Beane +;;;; + +(cl:in-package #:cl-user) +(cl:defpackage #:qlqs-user + (:use #:cl)) +(cl:in-package #:qlqs-user) + +(defpackage #:qlqs-info + (:export #:*version*)) + +(defvar qlqs-info:*version* "2015-01-28") + +(defpackage #:qlqs-impl + (:use #:cl) + (:export #:*implementation*) + (:export #:definterface + #:defimplementation) + (:export #:lisp + #:abcl + #:allegro + #:ccl + #:clasp + #:clisp + #:cmucl + #:cormanlisp + #:ecl + #:gcl + #:lispworks + #:mkcl + #:scl + #:sbcl)) + +(defpackage #:qlqs-impl-util + (:use #:cl #:qlqs-impl) + (:export #:call-with-quiet-compilation)) + +(defpackage #:qlqs-network + (:use #:cl #:qlqs-impl) + (:export #:open-connection + #:write-octets + #:read-octets + #:close-connection + #:with-connection)) + +(defpackage #:qlqs-progress + (:use #:cl) + (:export #:make-progress-bar + #:start-display + #:update-progress + #:finish-display)) + +(defpackage #:qlqs-http + (:use #:cl #:qlqs-network #:qlqs-progress) + (:export #:fetch + #:*proxy-url* + #:*maximum-redirects* + #:*default-url-defaults*)) + +(defpackage #:qlqs-minitar + (:use #:cl) + (:export #:unpack-tarball)) + +(defpackage #:quicklisp-quickstart + (:use #:cl #:qlqs-impl #:qlqs-impl-util #:qlqs-http #:qlqs-minitar) + (:export #:install + #:help + #:*proxy-url* + #:*asdf-url* + #:*quicklisp-tar-url* + #:*setup-url* + #:*help-message* + #:*after-load-message* + #:*after-initial-setup-message*)) + + +;;; +;;; Defining implementation-specific packages and functionality +;;; + +(in-package #:qlqs-impl) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun error-unimplemented (&rest args) + (declare (ignore args)) + (error "Not implemented"))) + +(defmacro neuter-package (name) + `(eval-when (:compile-toplevel :load-toplevel :execute) + (let ((definition (fdefinition 'error-unimplemented))) + (do-external-symbols (symbol ,(string name)) + (unless (fboundp symbol) + (setf (fdefinition symbol) definition)))))) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun feature-expression-passes-p (expression) + (cond ((keywordp expression) + (member expression *features*)) + ((consp expression) + (case (first expression) + (or + (some 'feature-expression-passes-p (rest expression))) + (and + (every 'feature-expression-passes-p (rest expression))))) + (t (error "Unrecognized feature expression -- ~S" expression))))) + + +(defmacro define-implementation-package (feature package-name &rest options) + (let* ((output-options '((:use) + (:export #:lisp))) + (prep (cdr (assoc :prep options))) + (class-option (cdr (assoc :class options))) + (class (first class-option)) + (superclasses (rest class-option)) + (import-options '()) + (effectivep (feature-expression-passes-p feature))) + (dolist (option options) + (ecase (first option) + ((:prep :class)) + ((:import-from + :import) + (push option import-options)) + ((:export + :shadow + :intern + :documentation) + (push option output-options)) + ((:reexport-from) + (push (cons :export (cddr option)) output-options) + (push (cons :import-from (cdr option)) import-options)))) + `(eval-when (:compile-toplevel :load-toplevel :execute) + ,@(when effectivep + prep) + (defclass ,class ,superclasses ()) + (defpackage ,package-name ,@output-options + ,@(when effectivep + import-options)) + ,@(when effectivep + `((setf *implementation* (make-instance ',class)))) + ,@(unless effectivep + `((neuter-package ,package-name)))))) + +(defmacro definterface (name lambda-list &body options) + (let* ((forbidden (intersection lambda-list lambda-list-keywords)) + (gf-options (remove :implementation options :key #'first)) + (implementations (set-difference options gf-options))) + (when forbidden + (error "~S not allowed in definterface lambda list" forbidden)) + (flet ((method-option (class body) + `(:method ((*implementation* ,class) ,@lambda-list) + ,@body))) + (let ((generic-name (intern (format nil "%~A" name)))) + `(eval-when (:compile-toplevel :load-toplevel :execute) + (defgeneric ,generic-name (lisp ,@lambda-list) + ,@gf-options + ,@(mapcar (lambda (implementation) + (destructuring-bind (class &rest body) + (rest implementation) + (method-option class body))) + implementations)) + (defun ,name ,lambda-list + (,generic-name *implementation* ,@lambda-list))))))) + +(defmacro defimplementation (name-and-options + lambda-list &body body) + (destructuring-bind (name &key (for t) qualifier) + (if (consp name-and-options) + name-and-options + (list name-and-options)) + (unless for + (error "You must specify an implementation name.")) + (let ((generic-name (find-symbol (format nil "%~A" name)))) + (unless (and generic-name + (fboundp generic-name)) + (error "~S does not name an implementation function" name)) + `(defmethod ,generic-name + ,@(when qualifier (list qualifier)) + ,(list* `(*implementation* ,for) lambda-list) ,@body)))) + + +;;; Bootstrap implementations + +(defvar *implementation* nil) +(defclass lisp () ()) + + +;;; Allegro Common Lisp + +(define-implementation-package :allegro #:qlqs-allegro + (:documentation + "Allegro Common Lisp - http://www.franz.com/products/allegrocl/") + (:class allegro) + (:reexport-from #:socket + #:make-socket) + (:reexport-from #:excl + #:read-vector)) + + +;;; Armed Bear Common Lisp + +(define-implementation-package :abcl #:qlqs-abcl + (:documentation + "Armed Bear Common Lisp - http://common-lisp.net/project/armedbear/") + (:class abcl) + (:reexport-from #:system + #:make-socket + #:get-socket-stream)) + +;;; Clozure CL + +(define-implementation-package :ccl #:qlqs-ccl + (:documentation + "Clozure Common Lisp - http://www.clozure.com/clozurecl.html") + (:class ccl) + (:reexport-from #:ccl + #:make-socket)) + + +;;; CLASP + +(define-implementation-package :clasp #:qlqs-clasp + (:documentation "CLASP - http://github.com/drmeister/clasp") + (:class clasp) + (:prep + (require 'sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:host-ent-address + #:socket-connect + #:socket-make-stream + #:inet-socket)) + + +;;; GNU CLISP + +(define-implementation-package :clisp #:qlqs-clisp + (:documentation "GNU CLISP - http://clisp.cons.org/") + (:class clisp) + (:reexport-from #:socket + #:socket-connect) + (:reexport-from #:ext + #:read-byte-sequence)) + + +;;; CMUCL + +(define-implementation-package :cmu #:qlqs-cmucl + (:documentation "CMU Common Lisp - http://www.cons.org/cmucl/") + (:class cmucl) + (:reexport-from #:ext + #:*gc-verbose*) + (:reexport-from #:system + #:make-fd-stream) + (:reexport-from #:extensions + #:connect-to-inet-socket)) + +(defvar qlqs-cmucl:*gc-verbose* nil) + + +;;; Scieneer CL + +(define-implementation-package :scl #:qlqs-scl + (:documentation "Scieneer Common Lisp - http://www.scieneer.com/scl/") + (:class scl) + (:reexport-from #:system + #:make-fd-stream) + (:reexport-from #:extensions + #:connect-to-inet-socket)) + +;;; ECL + +(define-implementation-package :ecl #:qlqs-ecl + (:documentation "ECL - http://ecls.sourceforge.net/") + (:class ecl) + (:prep + (require 'sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:host-ent-address + #:socket-connect + #:socket-make-stream + #:inet-socket)) + + +;;; LispWorks + +(define-implementation-package :lispworks #:qlqs-lispworks + (:documentation "LispWorks - http://www.lispworks.com/") + (:class lispworks) + (:prep + (require "comm")) + (:reexport-from #:comm + #:open-tcp-stream + #:get-host-entry)) + + +;;; SBCL + +(define-implementation-package :sbcl #:qlqs-sbcl + (:class sbcl) + (:documentation + "Steel Bank Common Lisp - http://www.sbcl.org/") + (:prep + (require 'sb-bsd-sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-ext + #:compiler-note) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:inet-socket + #:host-ent-address + #:socket-connect + #:socket-make-stream)) + +;;; MKCL + +(define-implementation-package :mkcl #:qlqs-mkcl + (:class mkcl) + (:documentation + "ManKai Common Lisp - http://common-lisp.net/project/mkcl/") + (:prep + (require 'sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:inet-socket + #:host-ent-address + #:socket-connect + #:socket-make-stream)) + +;;; +;;; Utility function +;;; + +(in-package #:qlqs-impl-util) + +(definterface call-with-quiet-compilation (fun) + (:implementation t + (let ((*load-verbose* nil) + (*compile-verbose* nil) + (*load-print* nil) + (*compile-print* nil)) + (handler-bind ((warning #'muffle-warning)) + (funcall fun))))) + +(defimplementation (call-with-quiet-compilation :for sbcl :qualifier :around) + (fun) + (declare (ignorable fun)) + (handler-bind ((qlqs-sbcl:compiler-note #'muffle-warning)) + (call-next-method))) + +(defimplementation (call-with-quiet-compilation :for cmucl :qualifier :around) + (fun) + (declare (ignorable fun)) + (let ((qlqs-cmucl:*gc-verbose* nil)) + (call-next-method))) + + +;;; +;;; Low-level networking implementations +;;; + +(in-package #:qlqs-network) + +(definterface host-address (host) + (:implementation t + host) + (:implementation mkcl + (qlqs-mkcl:host-ent-address (qlqs-mkcl:get-host-by-name host))) + (:implementation sbcl + (qlqs-sbcl:host-ent-address (qlqs-sbcl:get-host-by-name host)))) + +(definterface open-connection (host port) + (:implementation t + (declare (ignorable host port)) + (error "Sorry, quicklisp in implementation ~S is not supported yet." + (lisp-implementation-type))) + (:implementation allegro + (qlqs-allegro:make-socket :remote-host host + :remote-port port)) + (:implementation abcl + (let ((socket (qlqs-abcl:make-socket host port))) + (qlqs-abcl:get-socket-stream socket :element-type '(unsigned-byte 8)))) + (:implementation ccl + (qlqs-ccl:make-socket :remote-host host + :remote-port port)) + (:implementation clasp + (let* ((endpoint (qlqs-clasp:host-ent-address + (qlqs-clasp:get-host-by-name host))) + (socket (make-instance 'qlqs-clasp:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-clasp:socket-connect socket endpoint port) + (qlqs-clasp:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full))) + (:implementation clisp + (qlqs-clisp:socket-connect port host :element-type '(unsigned-byte 8))) + (:implementation cmucl + (let ((fd (qlqs-cmucl:connect-to-inet-socket host port))) + (qlqs-cmucl:make-fd-stream fd + :element-type '(unsigned-byte 8) + :binary-stream-p t + :input t + :output t))) + (:implementation scl + (let ((fd (qlqs-scl:connect-to-inet-socket host port))) + (qlqs-scl:make-fd-stream fd + :element-type '(unsigned-byte 8) + :input t + :output t))) + (:implementation ecl + (let* ((endpoint (qlqs-ecl:host-ent-address + (qlqs-ecl:get-host-by-name host))) + (socket (make-instance 'qlqs-ecl:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-ecl:socket-connect socket endpoint port) + (qlqs-ecl:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full))) + (:implementation lispworks + (qlqs-lispworks:open-tcp-stream host port + :direction :io + :errorp t + :read-timeout nil + :element-type '(unsigned-byte 8) + :timeout 5)) + (:implementation mkcl + (let* ((endpoint (qlqs-mkcl:host-ent-address + (qlqs-mkcl:get-host-by-name host))) + (socket (make-instance 'qlqs-mkcl:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-mkcl:socket-connect socket endpoint port) + (qlqs-mkcl:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full))) + (:implementation sbcl + (let* ((endpoint (qlqs-sbcl:host-ent-address + (qlqs-sbcl:get-host-by-name host))) + (socket (make-instance 'qlqs-sbcl:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-sbcl:socket-connect socket endpoint port) + (qlqs-sbcl:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full)))) + +(definterface read-octets (buffer connection) + (:implementation t + (read-sequence buffer connection)) + (:implementation allegro + (qlqs-allegro:read-vector buffer connection)) + (:implementation clisp + (qlqs-clisp:read-byte-sequence buffer connection + :no-hang nil + :interactive t))) + +(definterface write-octets (buffer connection) + (:implementation t + (write-sequence buffer connection) + (finish-output connection))) + +(definterface close-connection (connection) + (:implementation t + (ignore-errors (close connection)))) + +(definterface call-with-connection (host port fun) + (:implementation t + (let (connection) + (unwind-protect + (progn + (setf connection (open-connection host port)) + (funcall fun connection)) + (when connection + (close connection)))))) + +(defmacro with-connection ((connection host port) &body body) + `(call-with-connection ,host ,port (lambda (,connection) ,@body))) + + +;;; +;;; A text progress bar +;;; + +(in-package #:qlqs-progress) + +(defclass progress-bar () + ((start-time + :initarg :start-time + :accessor start-time) + (end-time + :initarg :end-time + :accessor end-time) + (progress-character + :initarg :progress-character + :accessor progress-character) + (character-count + :initarg :character-count + :accessor character-count + :documentation "How many characters wide is the progress bar?") + (characters-so-far + :initarg :characters-so-far + :accessor characters-so-far) + (update-interval + :initarg :update-interval + :accessor update-interval + :documentation "Update the progress bar display after this many + internal-time units.") + (last-update-time + :initarg :last-update-time + :accessor last-update-time + :documentation "The display was last updated at this time.") + (total + :initarg :total + :accessor total + :documentation "The total number of units tracked by this progress bar.") + (progress + :initarg :progress + :accessor progress + :documentation "How far in the progress are we?") + (pending + :initarg :pending + :accessor pending + :documentation "How many raw units should be tracked in the next + display update?")) + (:default-initargs + :progress-character #\= + :character-count 50 + :characters-so-far 0 + :update-interval (floor internal-time-units-per-second 4) + :last-update-time 0 + :total 0 + :progress 0 + :pending 0)) + +(defgeneric start-display (progress-bar)) +(defgeneric update-progress (progress-bar unit-count)) +(defgeneric update-display (progress-bar)) +(defgeneric finish-display (progress-bar)) +(defgeneric elapsed-time (progress-bar)) +(defgeneric units-per-second (progress-bar)) + +(defmethod start-display (progress-bar) + (setf (last-update-time progress-bar) (get-internal-real-time)) + (setf (start-time progress-bar) (get-internal-real-time)) + (fresh-line) + (finish-output)) + +(defmethod update-display (progress-bar) + (incf (progress progress-bar) (pending progress-bar)) + (setf (pending progress-bar) 0) + (setf (last-update-time progress-bar) (get-internal-real-time)) + (let* ((showable (floor (character-count progress-bar) + (/ (total progress-bar) (progress progress-bar)))) + (needed (- showable (characters-so-far progress-bar)))) + (setf (characters-so-far progress-bar) showable) + (dotimes (i needed) + (write-char (progress-character progress-bar))) + (finish-output))) + +(defmethod update-progress (progress-bar unit-count) + (incf (pending progress-bar) unit-count) + (let ((now (get-internal-real-time))) + (when (< (update-interval progress-bar) + (- now (last-update-time progress-bar))) + (update-display progress-bar)))) + +(defmethod finish-display (progress-bar) + (update-display progress-bar) + (setf (end-time progress-bar) (get-internal-real-time)) + (terpri) + (format t "~:D bytes in ~$ seconds (~$KB/sec)" + (total progress-bar) + (elapsed-time progress-bar) + (/ (units-per-second progress-bar) 1024)) + (finish-output)) + +(defmethod elapsed-time (progress-bar) + (/ (- (end-time progress-bar) (start-time progress-bar)) + internal-time-units-per-second)) + +(defmethod units-per-second (progress-bar) + (if (plusp (elapsed-time progress-bar)) + (/ (total progress-bar) (elapsed-time progress-bar)) + 0)) + +(defun kb/sec (progress-bar) + (/ (units-per-second progress-bar) 1024)) + + + +(defparameter *uncertain-progress-chars* "?") + +(defclass uncertain-size-progress-bar (progress-bar) + ((progress-char-index + :initarg :progress-char-index + :accessor progress-char-index) + (units-per-char + :initarg :units-per-char + :accessor units-per-char)) + (:default-initargs + :total 0 + :progress-char-index 0 + :units-per-char (floor (expt 1024 2) 50))) + +(defmethod update-progress :after ((progress-bar uncertain-size-progress-bar) + unit-count) + (incf (total progress-bar) unit-count)) + +(defmethod progress-character ((progress-bar uncertain-size-progress-bar)) + (let ((index (progress-char-index progress-bar))) + (prog1 + (char *uncertain-progress-chars* index) + (setf (progress-char-index progress-bar) + (mod (1+ index) (length *uncertain-progress-chars*)))))) + +(defmethod update-display ((progress-bar uncertain-size-progress-bar)) + (setf (last-update-time progress-bar) (get-internal-real-time)) + (multiple-value-bind (chars pend) + (floor (pending progress-bar) (units-per-char progress-bar)) + (setf (pending progress-bar) pend) + (dotimes (i chars) + (write-char (progress-character progress-bar)) + (incf (characters-so-far progress-bar)) + (when (<= (character-count progress-bar) + (characters-so-far progress-bar)) + (terpri) + (setf (characters-so-far progress-bar) 0) + (finish-output))) + (finish-output))) + +(defun make-progress-bar (total) + (if (or (not total) (zerop total)) + (make-instance 'uncertain-size-progress-bar) + (make-instance 'progress-bar :total total))) + +;;; +;;; A simple HTTP client +;;; + +(in-package #:qlqs-http) + +;;; Octet data + +(deftype octet () + '(unsigned-byte 8)) + +(defun make-octet-vector (size) + (make-array size :element-type 'octet + :initial-element 0)) + +(defun octet-vector (&rest octets) + (make-array (length octets) :element-type 'octet + :initial-contents octets)) + +;;; ASCII characters as integers + +(defun acode (char) + (cond ((eql char :cr) + 13) + ((eql char :lf) + 10) + (t + (let ((code (char-code char))) + (if (<= 0 code 127) + code + (error "Character ~S is not in the ASCII character set" + char)))))) + +(defvar *whitespace* + (list (acode #\Space) (acode #\Tab) (acode :cr) (acode :lf))) + +(defun whitep (code) + (member code *whitespace*)) + +(defun ascii-vector (string) + (let ((vector (make-octet-vector (length string)))) + (loop for char across string + for code = (char-code char) + for i from 0 + if (< 127 code) do + (error "Invalid character for ASCII -- ~A" char) + else + do (setf (aref vector i) code)) + vector)) + +(defun ascii-subseq (vector start end) + "Return a subseq of octet-specialized VECTOR as a string." + (let ((string (make-string (- end start)))) + (loop for i from 0 + for j from start below end + do (setf (char string i) (code-char (aref vector j)))) + string)) + +(defun ascii-downcase (code) + (if (<= 65 code 90) + (+ code 32) + code)) + +(defun ascii-equal (a b) + (eql (ascii-downcase a) (ascii-downcase b))) + +(defmacro acase (value &body cases) + (flet ((convert-case-keys (keys) + (mapcar (lambda (key) + (etypecase key + (integer key) + (character (char-code key)) + (symbol + (ecase key + (:cr 13) + (:lf 10) + ((t) t))))) + (if (consp keys) keys (list keys))))) + `(case ,value + ,@(mapcar (lambda (case) + (destructuring-bind (keys &rest body) + case + `(,(if (eql keys t) + t + (convert-case-keys keys)) + ,@body))) + cases)))) + +;;; Pattern matching (for finding headers) + +(defclass matcher () + ((pattern + :initarg :pattern + :reader pattern) + (pos + :initform 0 + :accessor match-pos) + (matchedp + :initform nil + :accessor matchedp))) + +(defun reset-match (matcher) + (setf (match-pos matcher) 0 + (matchedp matcher) nil)) + +(define-condition match-failure (error) ()) + +(defun match (matcher input &key (start 0) end error) + (let ((i start) + (end (or end (length input))) + (match-end (length (pattern matcher)))) + (with-slots (pattern pos) + matcher + (loop + (cond ((= pos match-end) + (let ((match-start (- i pos))) + (setf pos 0) + (setf (matchedp matcher) t) + (return (values match-start (+ match-start match-end))))) + ((= i end) + (return nil)) + ((= (aref pattern pos) + (aref input i)) + (incf i) + (incf pos)) + (t + (if error + (error 'match-failure) + (if (zerop pos) + (incf i) + (setf pos 0))))))))) + +(defun ascii-matcher (string) + (make-instance 'matcher + :pattern (ascii-vector string))) + +(defun octet-matcher (&rest octets) + (make-instance 'matcher + :pattern (apply 'octet-vector octets))) + +(defun acode-matcher (&rest codes) + (make-instance 'matcher + :pattern (make-array (length codes) + :element-type 'octet + :initial-contents + (mapcar 'acode codes)))) + + +;;; "Connection Buffers" are a kind of callback-driven, +;;; pattern-matching chunky stream. Callbacks can be called for a +;;; certain number of octets or until one or more patterns are seen in +;;; the input. cbufs automatically refill themselves from a +;;; connection as needed. + +(defvar *cbuf-buffer-size* 8192) + +(define-condition end-of-data (error) ()) + +(defclass cbuf () + ((data + :initarg :data + :accessor data) + (connection + :initarg :connection + :accessor connection) + (start + :initarg :start + :accessor start) + (end + :initarg :end + :accessor end) + (eofp + :initarg :eofp + :accessor eofp)) + (:default-initargs + :data (make-octet-vector *cbuf-buffer-size*) + :connection nil + :start 0 + :end 0 + :eofp nil) + (:documentation "A CBUF is a connection buffer that keeps track of + incoming data from a connection. Several functions make it easy to + treat a CBUF as a kind of chunky, callback-driven stream.")) + +(define-condition cbuf-progress () + ((size + :initarg :size + :accessor cbuf-progress-size + :initform 0))) + +(defun call-processor (fun cbuf start end) + (signal 'cbuf-progress :size (- end start)) + (funcall fun (data cbuf) start end)) + +(defun make-cbuf (connection) + (make-instance 'cbuf :connection connection)) + +(defun make-stream-writer (stream) + "Create a callback for writing data to STREAM." + (lambda (data start end) + (write-sequence data stream :start start :end end))) + +(defgeneric size (cbuf) + (:method ((cbuf cbuf)) + (- (end cbuf) (start cbuf)))) + +(defgeneric emptyp (cbuf) + (:method ((cbuf cbuf)) + (zerop (size cbuf)))) + +(defgeneric refill (cbuf) + (:method ((cbuf cbuf)) + (when (eofp cbuf) + (error 'end-of-data)) + (setf (start cbuf) 0) + (setf (end cbuf) + (read-octets (data cbuf) + (connection cbuf))) + (cond ((emptyp cbuf) + (setf (eofp cbuf) t) + (error 'end-of-data)) + (t (size cbuf))))) + +(defun process-all (fun cbuf) + (unless (emptyp cbuf) + (call-processor fun cbuf (start cbuf) (end cbuf)))) + +(defun multi-cmatch (matchers cbuf) + (let (start end) + (dolist (matcher matchers (values start end)) + (multiple-value-bind (s e) + (match matcher (data cbuf) + :start (start cbuf) + :end (end cbuf)) + (when (and s (or (null start) (< s start))) + (setf start s + end e)))))) + +(defun cmatch (matcher cbuf) + (if (consp matcher) + (multi-cmatch matcher cbuf) + (match matcher (data cbuf) :start (start cbuf) :end (end cbuf)))) + +(defun call-until-end (fun cbuf) + (handler-case + (loop + (process-all fun cbuf) + (refill cbuf)) + (end-of-data () + (return-from call-until-end)))) + +(defun show-cbuf (context cbuf) + (format t "cbuf: ~A ~D - ~D~%" context (start cbuf) (end cbuf))) + +(defun call-for-n-octets (n fun cbuf) + (let ((remaining n)) + (loop + (when (<= remaining (size cbuf)) + (let ((end (+ (start cbuf) remaining))) + (call-processor fun cbuf (start cbuf) end) + (setf (start cbuf) end) + (return))) + (process-all fun cbuf) + (decf remaining (size cbuf)) + (refill cbuf)))) + +(defun call-until-matching (matcher fun cbuf) + (loop + (multiple-value-bind (start end) + (cmatch matcher cbuf) + (when start + (call-processor fun cbuf (start cbuf) end) + (setf (start cbuf) end) + (return))) + (process-all fun cbuf) + (refill cbuf))) + +(defun ignore-data (data start end) + (declare (ignore data start end))) + +(defun skip-until-matching (matcher cbuf) + (call-until-matching matcher 'ignore-data cbuf)) + + +;;; Creating HTTP requests as octet buffers + +(defclass octet-sink () + ((storage + :initarg :storage + :accessor storage)) + (:default-initargs + :storage (make-array 1024 :element-type 'octet + :fill-pointer 0 + :adjustable t)) + (:documentation "A simple stream-like target for collecting + octets.")) + +(defun add-octet (octet sink) + (vector-push-extend octet (storage sink))) + +(defun add-octets (octets sink &key (start 0) end) + (setf end (or end (length octets))) + (loop for i from start below end + do (add-octet (aref octets i) sink))) + +(defun add-string (string sink) + (loop for char across string + for code = (char-code char) + do (add-octet code sink))) + +(defun add-strings (sink &rest strings) + (mapc (lambda (string) (add-string string sink)) strings)) + +(defun add-newline (sink) + (add-octet 13 sink) + (add-octet 10 sink)) + +(defun sink-buffer (sink) + (subseq (storage sink) 0)) + +(defvar *proxy-url* nil) + +(defun full-proxy-path (host port path) + (format nil "~:[http~;https~]://~A~:[:~D~;~*~]~A" + (= port 443) + host + (or (= port 80) + (= port 443)) + port + path)) + +(defun make-request-buffer (host port path &key (method "GET")) + (setf method (string method)) + (when *proxy-url* + (setf path (full-proxy-path host port path))) + (let ((sink (make-instance 'octet-sink))) + (flet ((add-line (&rest strings) + (apply #'add-strings sink strings) + (add-newline sink))) + (add-line method " " path " HTTP/1.1") + (add-line "Host: " host (if (= port 80) "" + (format nil ":~D" port))) + (add-line "Connection: close") + ;; FIXME: get this version string from somewhere else. + (add-line "User-Agent: quicklisp-bootstrap/" + qlqs-info:*version*) + (add-newline sink) + (sink-buffer sink)))) + +(defun sink-until-matching (matcher cbuf) + (let ((sink (make-instance 'octet-sink))) + (call-until-matching + matcher + (lambda (buffer start end) + (add-octets buffer sink :start start :end end)) + cbuf) + (sink-buffer sink))) + + +;;; HTTP headers + +(defclass header () + ((data + :initarg :data + :accessor data) + (status + :initarg :status + :accessor status) + (name-starts + :initarg :name-starts + :accessor name-starts) + (name-ends + :initarg :name-ends + :accessor name-ends) + (value-starts + :initarg :value-starts + :accessor value-starts) + (value-ends + :initarg :value-ends + :accessor value-ends))) + +(defmethod print-object ((header header) stream) + (print-unreadable-object (header stream :type t) + (prin1 (status header) stream))) + +(defun matches-at (pattern target pos) + (= (mismatch pattern target :start2 pos) (length pattern))) + +(defun header-value-indexes (field-name header) + (loop with data = (data header) + with pattern = (ascii-vector (string-downcase field-name)) + for start across (name-starts header) + for i from 0 + when (matches-at pattern data start) + return (values (aref (value-starts header) i) + (aref (value-ends header) i)))) + +(defun ascii-header-value (field-name header) + (multiple-value-bind (start end) + (header-value-indexes field-name header) + (when start + (ascii-subseq (data header) start end)))) + +(defun all-field-names (header) + (map 'list + (lambda (start end) + (ascii-subseq (data header) start end)) + (name-starts header) + (name-ends header))) + +(defun headers-alist (header) + (mapcar (lambda (name) + (cons name (ascii-header-value name header))) + (all-field-names header))) + +(defmethod describe-object :after ((header header) stream) + (format stream "~&Decoded headers:~% ~S~%" (headers-alist header))) + +(defun content-length (header) + (let ((field-value (ascii-header-value "content-length" header))) + (when field-value + (let ((value (ignore-errors (parse-integer field-value)))) + (or value + (error "Content-Length header field value is not a number -- ~A" + field-value)))))) + +(defun chunkedp (header) + (string= (ascii-header-value "transfer-encoding" header) "chunked")) + +(defun location (header) + (ascii-header-value "location" header)) + +(defun status-code (vector) + (let* ((space (position (acode #\Space) vector)) + (c1 (- (aref vector (incf space)) 48)) + (c2 (- (aref vector (incf space)) 48)) + (c3 (- (aref vector (incf space)) 48))) + (+ (* c1 100) + (* c2 10) + (* c3 1)))) + +(defun force-downcase-field-names (header) + (loop with data = (data header) + for start across (name-starts header) + for end across (name-ends header) + do (loop for i from start below end + for code = (aref data i) + do (setf (aref data i) (ascii-downcase code))))) + +(defun skip-white-forward (pos vector) + (position-if-not 'whitep vector :start pos)) + +(defun skip-white-backward (pos vector) + (let ((nonwhite (position-if-not 'whitep vector :end pos :from-end t))) + (if nonwhite + (1+ nonwhite) + pos))) + +(defun contract-field-value-indexes (header) + "Header field values exclude leading and trailing whitespace; adjust +the indexes in the header accordingly." + (loop with starts = (value-starts header) + with ends = (value-ends header) + with data = (data header) + for i from 0 + for start across starts + for end across ends + do + (setf (aref starts i) (skip-white-forward start data)) + (setf (aref ends i) (skip-white-backward end data)))) + +(defun next-line-pos (vector) + (let ((pos 0)) + (labels ((finish (&optional (i pos)) + (return-from next-line-pos i)) + (after-cr (code) + (acase code + (:lf (finish pos)) + (t (finish (1- pos))))) + (pending (code) + (acase code + (:cr #'after-cr) + (:lf (finish pos)) + (t #'pending)))) + (let ((state #'pending)) + (loop + (setf state (funcall state (aref vector pos))) + (incf pos)))))) + +(defun make-hvector () + (make-array 16 :fill-pointer 0 :adjustable t)) + +(defun process-header (vector) + "Create a HEADER instance from the octet data in VECTOR." + (let* ((name-starts (make-hvector)) + (name-ends (make-hvector)) + (value-starts (make-hvector)) + (value-ends (make-hvector)) + (header (make-instance 'header + :data vector + :status 999 + :name-starts name-starts + :name-ends name-ends + :value-starts value-starts + :value-ends value-ends)) + (mark nil) + (pos (next-line-pos vector))) + (unless pos + (error "Unable to process HTTP header")) + (setf (status header) (status-code vector)) + (labels ((save (value vector) + (vector-push-extend value vector)) + (mark () + (setf mark pos)) + (clear-mark () + (setf mark nil)) + (finish () + (if mark + (save mark value-ends) + (save pos value-ends)) + (force-downcase-field-names header) + (contract-field-value-indexes header) + (return-from process-header header)) + (in-new-line (code) + (acase code + ((#\Tab #\Space) (setf mark nil) #'in-value) + (t + (when mark + (save mark value-ends)) + (clear-mark) + (save pos name-starts) + (in-name code)))) + (after-cr (code) + (acase code + (:lf #'in-new-line) + (t (in-new-line code)))) + (pending-value (code) + (acase code + ((#\Tab #\Space) #'pending-value) + (:cr #'after-cr) + (:lf #'in-new-line) + (t (save pos value-starts) #'in-value))) + (in-name (code) + (acase code + (#\: + (save pos name-ends) + (save (1+ pos) value-starts) + #'in-value) + ((:cr :lf) + (finish)) + ((#\Tab #\Space) + (error "Unexpected whitespace in header field name")) + (t + (unless (<= 0 code 127) + (error "Unexpected non-ASCII header field name")) + #'in-name))) + (in-value (code) + (acase code + (:lf (mark) #'in-new-line) + (:cr (mark) #'after-cr) + (t #'in-value)))) + (let ((state #'in-new-line)) + (loop + (incf pos) + (when (<= (length vector) pos) + (error "No header found in response")) + (setf state (funcall state (aref vector pos)))))))) + + +;;; HTTP URL parsing + +(defclass url () + ((hostname + :initarg :hostname + :accessor hostname + :initform nil) + (port + :initarg :port + :accessor port + :initform 80) + (path + :initarg :path + :accessor path + :initform "/"))) + +(defun parse-urlstring (urlstring) + (setf urlstring (string-trim " " urlstring)) + (let* ((pos (mismatch urlstring "http://" :test 'char-equal)) + (mark pos) + (url (make-instance 'url))) + (labels ((save () + (subseq urlstring mark pos)) + (mark () + (setf mark pos)) + (finish () + (return-from parse-urlstring url)) + (hostname-char-p (char) + (position char "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_." + :test 'char-equal)) + (at-start (char) + (case char + (#\/ + (setf (port url) nil) + (mark) + #'in-path) + (t + #'in-host))) + (in-host (char) + (case char + ((#\/ :end) + (setf (hostname url) (save)) + (mark) + #'in-path) + (#\: + (setf (hostname url) (save)) + (mark) + #'in-port) + (t + (unless (hostname-char-p char) + (error "~S is not a valid URL" urlstring)) + #'in-host))) + (in-port (char) + (case char + ((#\/ :end) + (setf (port url) + (parse-integer urlstring + :start (1+ mark) + :end pos)) + (mark) + #'in-path) + (t + (unless (digit-char-p char) + (error "Bad port in URL ~S" urlstring)) + #'in-port))) + (in-path (char) + (case char + ((#\# :end) + (setf (path url) (save)) + (finish))) + #'in-path)) + (let ((state #'at-start)) + (loop + (when (<= (length urlstring) pos) + (funcall state :end) + (finish)) + (setf state (funcall state (aref urlstring pos))) + (incf pos)))))) + +(defun url (thing) + (if (stringp thing) + (parse-urlstring thing) + thing)) + +(defgeneric request-buffer (method url) + (:method (method url) + (setf url (url url)) + (make-request-buffer (hostname url) (port url) (path url) + :method method))) + +(defun urlstring (url) + (format nil "~@[http://~A~]~@[:~D~]~A" + (hostname url) + (and (/= 80 (port url)) (port url)) + (path url))) + +(defmethod print-object ((url url) stream) + (print-unreadable-object (url stream :type t) + (prin1 (urlstring url) stream))) + +(defun merge-urls (url1 url2) + (setf url1 (url url1)) + (setf url2 (url url2)) + (make-instance 'url + :hostname (or (hostname url1) + (hostname url2)) + :port (or (port url1) + (port url2)) + :path (or (path url1) + (path url2)))) + + +;;; Requesting an URL and saving it to a file + +(defparameter *maximum-redirects* 10) +(defvar *default-url-defaults* (url "http://src.quicklisp.org/")) + +(defun read-http-header (cbuf) + (let ((header-data (sink-until-matching (list (acode-matcher :lf :lf) + (acode-matcher :cr :cr) + (acode-matcher :cr :lf :cr :lf)) + cbuf))) + (process-header header-data))) + +(defun read-chunk-header (cbuf) + (let* ((header-data (sink-until-matching (acode-matcher :cr :lf) cbuf)) + (end (or (position (acode :cr) header-data) + (position (acode #\;) header-data)))) + (values (parse-integer (ascii-subseq header-data 0 end) :radix 16)))) + +(defun save-chunk-response (stream cbuf) + "For a chunked response, read all chunks and write them to STREAM." + (let ((fun (make-stream-writer stream)) + (matcher (acode-matcher :cr :lf))) + (loop + (let ((chunk-size (read-chunk-header cbuf))) + (when (zerop chunk-size) + (return)) + (call-for-n-octets chunk-size fun cbuf) + (skip-until-matching matcher cbuf))))) + +(defun save-response (file header cbuf) + (with-open-file (stream file + :direction :output + :if-exists :supersede + :element-type 'octet) + (let ((content-length (content-length header))) + (cond ((chunkedp header) + (save-chunk-response stream cbuf)) + (content-length + (call-for-n-octets content-length + (make-stream-writer stream) + cbuf)) + (t + (call-until-end (make-stream-writer stream) cbuf)))))) + +(defun call-with-progress-bar (size fun) + (let ((progress-bar (make-progress-bar size))) + (start-display progress-bar) + (flet ((update (condition) + (update-progress progress-bar + (cbuf-progress-size condition)))) + (handler-bind ((cbuf-progress #'update)) + (funcall fun))) + (finish-display progress-bar))) + +(defun fetch (url file &key (follow-redirects t) quietly + (maximum-redirects *maximum-redirects*)) + "Request URL and write the body of the response to FILE." + (setf url (merge-urls url *default-url-defaults*)) + (setf file (merge-pathnames file)) + (let ((redirect-count 0) + (original-url url) + (connect-url (or (url *proxy-url*) url)) + (stream (if quietly + (make-broadcast-stream) + *trace-output*))) + (loop + (when (<= maximum-redirects redirect-count) + (error "Too many redirects for ~A" original-url)) + (with-connection (connection (hostname connect-url) (port connect-url)) + (let ((cbuf (make-instance 'cbuf :connection connection)) + (request (request-buffer "GET" url))) + (write-octets request connection) + (let ((header (read-http-header cbuf))) + (loop while (= (status header) 100) + do (setf header (read-http-header cbuf))) + (cond ((= (status header) 200) + (let ((size (content-length header))) + (format stream "~&; Fetching ~A~%" url) + (if (and (numberp size) + (plusp size)) + (format stream "; ~$KB~%" (/ size 1024)) + (format stream "; Unknown size~%")) + (if quietly + (save-response file header cbuf) + (call-with-progress-bar (content-length header) + (lambda () + (save-response file header cbuf)))))) + ((not (<= 300 (status header) 399)) + (error "Unexpected status for ~A: ~A" + url (status header)))) + (if (and follow-redirects (<= 300 (status header) 399)) + (let ((new-urlstring (ascii-header-value "location" header))) + (when (not new-urlstring) + (error "Redirect code ~D received, but no Location: header" + (status header))) + (incf redirect-count) + (setf url (merge-urls new-urlstring + url)) + (format stream "~&; Redirecting to ~A~%" url)) + (return (values header (and file (probe-file file))))))))))) + + +;;; A primitive tar unpacker + +(in-package #:qlqs-minitar) + +(defun make-block-buffer () + (make-array 512 :element-type '(unsigned-byte 8) :initial-element 0)) + +(defun skip-n-blocks (n stream) + (let ((block (make-block-buffer))) + (dotimes (i n) + (read-sequence block stream)))) + +(defun ascii-subseq (vector start end) + (let ((string (make-string (- end start)))) + (loop for i from 0 + for j from start below end + do (setf (char string i) (code-char (aref vector j)))) + string)) + +(defun block-asciiz-string (block start length) + (let* ((end (+ start length)) + (eos (or (position 0 block :start start :end end) + end))) + (ascii-subseq block start eos))) + +(defun prefix (header) + (when (plusp (aref header 345)) + (block-asciiz-string header 345 155))) + +(defun name (header) + (block-asciiz-string header 0 100)) + +(defun payload-size (header) + (values (parse-integer (block-asciiz-string header 124 12) :radix 8))) + +(defun nth-block (n file) + (with-open-file (stream file :element-type '(unsigned-byte 8)) + (let ((block (make-block-buffer))) + (skip-n-blocks (1- n) stream) + (read-sequence block stream) + block))) + +(defun payload-type (code) + (case code + (0 :file) + (48 :file) + (53 :directory) + (t :unsupported))) + +(defun full-path (header) + (let ((prefix (prefix header)) + (name (name header))) + (if prefix + (format nil "~A/~A" prefix name) + name))) + +(defun save-file (file size stream) + (multiple-value-bind (full-blocks partial) + (truncate size 512) + (ensure-directories-exist file) + (with-open-file (outstream file + :direction :output + :if-exists :supersede + :element-type '(unsigned-byte 8)) + (let ((block (make-block-buffer))) + (dotimes (i full-blocks) + (read-sequence block stream) + (write-sequence block outstream)) + (when (plusp partial) + (read-sequence block stream) + (write-sequence block outstream :end partial)))))) + +(defun unpack-tarball (tarfile &key (directory *default-pathname-defaults*)) + (let ((block (make-block-buffer))) + (with-open-file (stream tarfile :element-type '(unsigned-byte 8)) + (loop + (let ((size (read-sequence block stream))) + (when (zerop size) + (return)) + (unless (= size 512) + (error "Bad size on tarfile")) + (when (every #'zerop block) + (return)) + (let* ((payload-code (aref block 156)) + (payload-type (payload-type payload-code)) + (tar-path (full-path block)) + (full-path (merge-pathnames tar-path directory)) + (payload-size (payload-size block))) + (case payload-type + (:file + (save-file full-path payload-size stream)) + (:directory + (ensure-directories-exist full-path)) + (t + (warn "Unknown tar block payload code -- ~D" payload-code) + (skip-n-blocks (ceiling (payload-size block) 512) stream))))))))) + +(defun contents (tarfile) + (let ((block (make-block-buffer)) + (result '())) + (with-open-file (stream tarfile :element-type '(unsigned-byte 8)) + (loop + (let ((size (read-sequence block stream))) + (when (zerop size) + (return (nreverse result))) + (unless (= size 512) + (error "Bad size on tarfile")) + (when (every #'zerop block) + (return (nreverse result))) + (let* ((payload-type (payload-type (aref block 156))) + (tar-path (full-path block)) + (payload-size (payload-size block))) + (skip-n-blocks (ceiling payload-size 512) stream) + (case payload-type + (:file + (push tar-path result)) + (:directory + (push tar-path result))))))))) + + +;;; +;;; The actual bootstrapping work +;;; + +(in-package #:quicklisp-quickstart) + +(defvar *home* + (merge-pathnames (make-pathname :directory '(:relative "quicklisp")) + (user-homedir-pathname))) + +(defun qmerge (pathname) + (merge-pathnames pathname *home*)) + +(defun renaming-fetch (url file) + (let ((tmpfile (qmerge "tmp/fetch.dat"))) + (fetch url tmpfile) + (rename-file tmpfile file))) + +(defvar *quickstart-parameters* nil + "This plist is populated with parameters that may carry over to the + initial configuration of the client, e.g. :proxy-url + or :initial-dist-url") + +(defvar *quicklisp-hostname* "beta.quicklisp.org") + +(defvar *client-info-url* + (format nil "http://~A/client/quicklisp.sexp" + *quicklisp-hostname*)) + +(defclass client-info () + ((setup-url + :reader setup-url + :initarg :setup-url) + (asdf-url + :reader asdf-url + :initarg :asdf-url) + (client-tar-url + :reader client-tar-url + :initarg :client-tar-url) + (version + :reader version + :initarg :version) + (plist + :reader plist + :initarg :plist) + (source-file + :reader source-file + :initarg :source-file))) + +(defmethod print-object ((client-info client-info) stream) + (print-unreadable-object (client-info stream :type t) + (prin1 (version client-info) stream))) + +(defun safely-read (stream) + (let ((*read-eval* nil)) + (read stream))) + +(defun fetch-client-info-plist (url) + "Fetch and return the client info data at URL." + (let ((local-client-info-file (qmerge "tmp/client-info.sexp"))) + (ensure-directories-exist local-client-info-file) + (renaming-fetch url local-client-info-file) + (with-open-file (stream local-client-info-file) + (list* :source-file local-client-info-file + (safely-read stream))))) + +(defun fetch-client-info (url) + (let ((plist (fetch-client-info-plist url))) + (destructuring-bind (&key setup asdf client-tar version + source-file + &allow-other-keys) + plist + (unless (and setup asdf client-tar version) + (error "Invalid data from client info URL -- ~A" url)) + (make-instance 'client-info + :setup-url (getf setup :url) + :asdf-url (getf asdf :url) + :client-tar-url (getf client-tar :url) + :version version + :plist plist + :source-file source-file)))) + +(defun client-info-url-from-version (version) + (format nil "http://~A/client/~A/client-info.sexp" + *quicklisp-hostname* + version)) + +(defun distinfo-url-from-version (version) + (format nil "http://~A/dist/~A/distinfo.txt" + *quicklisp-hostname* + version)) + +(defvar *help-message* + (format nil "~&~% ==== quicklisp quickstart install help ====~%~% ~ + quicklisp-quickstart:install can take the following ~ + optional arguments:~%~% ~ + :path \"/path/to/installation/\"~%~% ~ + :proxy \"http://your.proxy:port/\"~%~% ~ + :client-url ~%~% ~ + :client-version ~%~% ~ + :dist-url ~%~% ~ + :dist-version ~%~%")) + +(defvar *after-load-message* + (format nil "~&~% ==== quicklisp quickstart ~A loaded ====~%~% ~ + To continue with installation, evaluate: (quicklisp-quickstart:install)~%~% ~ + For installation options, evaluate: (quicklisp-quickstart:help)~%~%" + qlqs-info:*version*)) + +(defvar *after-initial-setup-message* + (with-output-to-string (*standard-output*) + (format t "~&~% ==== quicklisp installed ====~%~%") + (format t " To load a system, use: (ql:quickload \"system-name\")~%~%") + (format t " To find systems, use: (ql:system-apropos \"term\")~%~%") + (format t " To load Quicklisp every time you start Lisp, use: (ql:add-to-init-file)~%~%") + (format t " For more information, see http://www.quicklisp.org/beta/~%~%"))) + +(defun initial-install (&key (client-url *client-info-url*) dist-url) + (setf *quickstart-parameters* + (list :proxy-url *proxy-url* + :initial-dist-url dist-url)) + (ensure-directories-exist (qmerge "tmp/")) + (let ((client-info (fetch-client-info client-url)) + (tmptar (qmerge "tmp/quicklisp.tar")) + (setup (qmerge "setup.lisp")) + (asdf (qmerge "asdf.lisp"))) + (renaming-fetch (client-tar-url client-info) tmptar) + (unpack-tarball tmptar :directory (qmerge "./")) + (renaming-fetch (setup-url client-info) setup) + (renaming-fetch (asdf-url client-info) asdf) + (rename-file (source-file client-info) (qmerge "client-info.sexp")) + (load setup :verbose nil :print nil) + (write-string *after-initial-setup-message*) + (finish-output))) + +(defun help () + (write-string *help-message*) + t) + +(defun non-empty-file-namestring (pathname) + (let ((string (file-namestring pathname))) + (unless (or (null string) + (equal string "")) + string))) + +(defun install (&key ((:path *home*) *home*) + ((:proxy *proxy-url*) *proxy-url*) + client-url + client-version + dist-url + dist-version) + (setf *home* (merge-pathnames *home* (truename *default-pathname-defaults*))) + (let ((name (non-empty-file-namestring *home*))) + (when name + (warn "Making ~A part of the install pathname directory" + name) + ;; This corrects a pathname like "/foo/bar" to "/foo/bar/" and + ;; "foo" to "foo/" + (setf *home* + (make-pathname :defaults *home* + :directory (append (pathname-directory *home*) + (list name)))))) + (let ((setup-file (qmerge "setup.lisp"))) + (when (probe-file setup-file) + (multiple-value-bind (result proceed) + (with-simple-restart (load-setup "Load ~S" setup-file) + (error "Quicklisp has already been installed. Load ~S instead." + setup-file)) + (declare (ignore result)) + (when proceed + (return-from install (load setup-file)))))) + (if (find-package '#:ql) + (progn + (write-line "!!! Quicklisp has already been set up. !!!") + (write-string *after-initial-setup-message*) + t) + (call-with-quiet-compilation + (lambda () + (let ((client-url (or client-url + (and client-version + (client-info-url-from-version client-version)) + *client-info-url*)) + ;; It's ok for dist-url to be nil; there's a default in + ;; the client + (dist-url (or dist-url + (and dist-version + (distinfo-url-from-version dist-version))))) + (initial-install :client-url client-url + :dist-url dist-url)))))) + +(write-string *after-load-message*) + +;;; End of quicklisp.lisp diff --git a/scripts/shell.nix b/scripts/shell.nix deleted file mode 100755 index 02ebd04e..00000000 --- a/scripts/shell.nix +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env nix-shell -#!nix-shell ./shell.nix --command "rlwrap sbcl --noinform --eval '(ql:quickload :breeze)' --eval '(br:main)'" - -with import {}; -stdenv.mkDerivation rec { - name = "env"; - env = buildEnv { name = name; paths = buildInputs; }; - buildInputs = [ - rlwrap - sbcl - ]; -} diff --git a/scripts/test.sh b/scripts/test.sh index 6508e898..d07a87fa 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,6 +3,8 @@ # This script is used to run the tests # +set -e + cd "$(git rev-parse --show-toplevel)" exec sbcl --noinform --non-interactive \ diff --git a/src/analysis.lisp b/src/analysis.lisp new file mode 100644 index 00000000..3a1201e9 --- /dev/null +++ b/src/analysis.lisp @@ -0,0 +1,387 @@ + +(uiop:define-package #:breeze.analysis + (:documentation "Linter, formatter, and maybe more.") + (:use #:cl) + (:use-reexport #:breeze.lossless-reader #:breeze.pattern) + ;; Tree/Form predicate + (:export + #:in-package-node-p + #:find-node + #:find-path-to-position) + (:export + #:in-package-node-p + #:lint)) + +(in-package #:breeze.analysis) + + +;;; Basic utilities for nodes + +(defun node-length (node) + "Returns the number of children of NODE, or nil if it doesn't have any +children nodes." + (let ((children (node-children node))) + (when (listp children) + (length children)))) + +(defun node-string= (state node string) + (string= (source state) string + :start1 (node-start node) + :end1 (node-end node))) + +(defun node-string-equal (state node string) + (string-equal (source state) string + :start1 (node-start node) + :end1 (node-end node))) + + +;;; Integrating pattern.lisp and lossless-parser.lisp + +(defparameter *state* nil + "The parser state associated with the node currently being matched.") + +;; (defpattern in-package package-designator) + +(defun match-node (pattern state node) + (let ((*state* state)) + (match pattern node))) + +(defun match-parser-state (pattern state) + (let* ((*state* state)) + (match pattern (tree state)))) + +(defmethod match (pattern (state state)) + (match-parser-state pattern state)) + +(defun plusp* (x) + (and (numberp x) (plusp x))) + +(defun match-symbol-to-token (symbol token-node &aux (state *state*)) + (and + (symbolp symbol) + (token-node-p token-node) + (let* ((name (symbol-name symbol)) + (package (symbol-package symbol)) + ;; TODO would be nice to cache this + (symbol-node (token-symbol-node state token-node))) + ;; TODO use case-sensitive comparison, but convert case if + ;; necessary (i.e. depending on *read-case*) + (when symbol-node + (and + (ecase (node-type symbol-node) + (current-package-symbol (node-string-equal state token-node name)) + (keyword + (and (string-equal "KEYWORD" (package-name package)) + (node-string-equal state symbol-node name))) + (uninterned-symbol + (and (null package) + (node-string-equal state symbol-node name))) + ((qualified-symbol possibly-internal-symbol) + (destructuring-bind (package-name-node symbol-name-node) + (node-children symbol-node) + (and + (node-string-equal state symbol-name-node name) + (some (lambda (package-name) + (node-string-equal state package-name-node package-name)) + `(,(package-name package) + ,@(package-nicknames package))))))) + ;; symbol-node + t))))) + +;; TODO add a special pattern type to match symbols in packages that +;; are not defined in the current image. +(defmethod match ((pattern symbol) (node node)) + (match-symbol-to-token pattern node)) + +(defmethod match ((pattern null) (node node)) + (match-symbol-to-token pattern node)) + +(defmethod match ((pattern term) (state state)) + (match-parser-state pattern state)) + +;; TODO package-local-nicknames + + +;; TODO One method per type of node + +(defmethod match (pattern (node node)) + (case (node-type node) + ;; Recurse into nodes of type "parens" + (parens (match pattern (node-children node))) + (t (call-next-method)))) + +#++ +(progn + whitespace + block-comment + line-comment + token + parens + punctuation + string + quote + quasiquote + dot + comma + sharp + sharp-char + sharp-function + sharp-vector + sharp-bitvector + sharp-uninterned + sharp-eval + sharp-binary + sharp-octal + sharp-hexa + sharp-complex + sharp-structure + sharp-pathname + sharp-feature + sharp-feature-not + sharp-radix + sharp-array + sharp-label + sharp-reference + sharp-unknown) + + +;;; Basic tree inspection + +;; TODO I want to check if a node is an "in-package" node... +;; - case converting if necessary +;; - skip whitespaces +;; - check if there's a package designator +;; +;; Now, I have a chicken-and-egg issue because of +;; package-local-nicknames... I need to know what is the current +;; package to look for PLNs to find the in-pacakge form, but I need +;; the in-package to know the current package. + +(defmacro define-node-matcher (name (pattern) &body body) + `(defun ,name (state node) + ,(format nil "Does NODE match ~s?" pattern) + (let* ((*state* state) + (*match-skip* #'whitespace-or-comment-node-p) + (bindings (match (compile-pattern ,pattern) node))) + ,@body))) + +#++ ;; TODO +(define-node-matcher in-package-node-p ('(in-package :?package-designator)) + (when bindings + (second bindings) + #++ + (destructuring-bind (&key ?package-designator) bindings + ?package-designator))) + +(defun in-package-node-p (state node) + "Is NODE a cl:in-package node?" + (let* ((*state* state) + (*match-skip* #'whitespace-or-comment-node-p) + (bindings (match #.(compile-pattern `(in-package :?package)) node)) + (package-designator-node (cdr (find-binding bindings :?package)))) + package-designator-node)) + +#++ (compile-pattern '(if :?cond :?then :?else :?extra (:zero-or-more :?extras))) + +(define-node-matcher malformed-if-node-p ('(if :?cond :?then :?else :?extra (:zero-or-more :?extras))) + (when bindings + ;; (destructuring-bind (&key ?cond ?then ?else ?extra) bindings) + t)) + +(defun find-node (position nodes) + "Given a list of NODES, return which node contains the POSITION." + (when (listp nodes) + (loop :for node :in nodes + :for start = (node-start node) + :for end = (node-end node) + :for i :from 0 + :when (and (<= start end) (< position end)) + :do (return (cons node i))))) + +(defun find-path-to-position (state position) + "Given a list of NODES, return a path (list of cons (node . index))" + (loop :for found = (find-node position (tree state)) + :then (find-node position (node-children (car found))) + #++ (let ((node (car found))) + (and (listp (node-content state node)) + ;; (car (node-content state node)) + (find-node position (node-content state node)))) + :while found + :collect found)) + + +;;; Trying to figure out how to run the "formatting rules" without +;;; applying them... + + +;; TODO try to keep track wheter the current node is quoted or not +(defun %walk (state callback tree depth quotedp) + (when tree + (flet ((cb (node &rest args) + "Call callback with NODE, DEPTH and ARGS." + (apply callback node :depth depth args))) + (etypecase tree + (list + (loop + :for i :from 0 + :for previous = nil :then (first rest) + :for rest :on tree + :for node = (car rest) + ;; Recurse + :collect (%walk state + callback + (cb node + :aroundp t + :nth i + :firstp (eq tree rest) + :lastp (null (cdr rest)) + :previous previous + :quotedp quotedp) + (1+ depth) + quotedp))) + (node + (case (node-type tree) + (parens + (cb tree :beforep t :quotedp quotedp) + (%walk state + callback + (node-children tree) + (1+ depth) + quotedp) + (cb tree :afterp t :quotedp quotedp)) + (t + (cb tree)))))))) + +(defun walk (state callback &optional quotedp) + "Call CALLBACK over all nodes in the parse tree contained by STATE. +CALLBACK will be called multiple times on the same node, with +different parameters. + +When CALLBACK is called with :aroundp t, the CALLBACK can decide to +stop the walk here(i.e. not recurse) by returning nil. The CALLBACK +can also return a new node altogether, the walk will +continue (recurse) in this new node instead. + +" + (%walk state callback (tree state) 0 quotedp)) + +;; This is equivalent to unparse with the leading and trailing +;; whitespace fixes. It is _much_ more succint! +#++ +(let ((state (parse " (+ 2) "))) + (with-output-to-string (out) + (walk state (lambda (node &rest args &key depth aroundp beforep afterp + firstp lastp nth) + ;; Debug info + (format t "~&~s ~{~s~^ ~}" node args) + ;; Printing stuff + (cond + (beforep + (write-char #\( out)) + (afterp + (write-char #\) out)) + ((not (or aroundp beforep afterp)) + (write-node node state out))) + ;; Removing useless whitespaces + (unless (and (plusp depth) + aroundp + (whitespace-node-p node) + (or firstp lastp)) + node))))) + + +;;; Utilities to collect "diagnostics" + +(defvar *diagnostics* nil) +(defvar *point-max* nil) + +(defun make-diagnostic (start end severity format-string format-args) + "Create a \"diagnostic\" object." + (list start (if (= +end+ end) *point-max* end) + severity + (apply #'format nil format-string format-args))) + +(defun push-diagnostic* (start end severity format-string format-args) + "Create a diagnostic object and push it into the special variable +*diagnostics*." + (push + (make-diagnostic start end + severity + format-string format-args) + *diagnostics*)) + +;; Same as push-diagnostic*, but takes a &rest +(defun push-diagnostic (start end severity format-string &rest format-args) + "Create a diagnostic object and push it into the special variable +*diagnostics*." + (push-diagnostic* start end severity format-string format-args)) + +(defun diag-node (node severity format-string &rest format-args) + (push-diagnostic* (node-start node) (node-end node) + severity format-string format-args)) + +(defun diag-warn (node format-string &rest format-args) + (apply #'diag-node node :warning format-string format-args)) + +(defun diag-error (node format-string &rest format-args) + (apply #'diag-node node :error format-string format-args)) + + + +(defun warn-undefined-in-package (state node) + (alexandria:when-let ((package-designator-node (in-package-node-p state node))) + + (let* ((package-designator (read-from-string (node-content state package-designator-node)))) + (when (and (typep package-designator 'breeze.utils:string-designator) + (null (find-package package-designator))) + (diag-warn + node + "Package ~s is not currently defined." package-designator))))) + +(defun warn-extraneous-whitespaces (state node firstp lastp previous) + (cond + ((and firstp lastp) + (diag-warn node "Extraneous whitespaces.")) + (firstp + (diag-warn node "Extraneous leading whitespaces.")) + ((and lastp (not (line-comment-node-p previous))) + (diag-warn node "Extraneous trailing whitespaces.")) + ((and (not (or firstp lastp)) + ;; Longer than 1 + (< 1 (- (node-end node) (node-start node))) + ;; "contains no newline" + (not (position #\Newline + (source state) + :start (node-start node) + :end (node-end node)))) + (diag-warn node "Extraneous internal whitespaces.")))) + +(defun error-invalid-node (node) + (unless (valid-node-p node) + (diag-error node "Syntax error"))) + +(defun lint (&key buffer-string point-max &allow-other-keys + &aux + (state (parse buffer-string)) + (*diagnostics* '()) + (*point-max* (or point-max (length buffer-string)))) + (walk state + (lambda (node &rest args &key depth aroundp beforep afterp + firstp lastp nth + previous + quotedp + &allow-other-keys) + (declare (ignorable depth beforep afterp nth args quotedp)) + ;; Debug info + ;; (format *debug-io* "~&~s ~{~s~^ ~}" node args) + (when aroundp + (error-invalid-node node) + (warn-undefined-in-package state node) + (when (and (plusp depth) + (whitespace-node-p node)) + (warn-extraneous-whitespaces state node firstp lastp previous))) + ;; Always return the node, we don't want to modify it. + ;; Technically, we could return nil to avoid recursing into + ;; the node (when aroundp is true, that is). + node)) + *diagnostics*) diff --git a/src/asdf.lisp b/src/asdf.lisp index 6c110a1f..9973eec2 100644 --- a/src/asdf.lisp +++ b/src/asdf.lisp @@ -1,12 +1,13 @@ -;; https://asdf.common-lisp.dev/asdf.html -;; https://quickdocs.org/asdf-dependency-graph +;;;; Utilities for ASDF (uiop:define-package #:breeze.asdf - (:documentation "Utilities for adsf") + (:documentation "Utilities for asdf") (:nicknames #:basdf) (:use :cl #:alexandria) (:export #:system-files + #:find-all-related-systems + #:find-all-related-files #:clear-fasl #:reload-system #:recompile-system @@ -15,6 +16,18 @@ (in-package #:breeze.asdf) +(defun find-all-related-systems (system) + "Given a system, find all systems defined in the same system definition +file (including the one passed as argument)." + (let ((result ()) + (asd-pathname (asdf:system-source-file system))) + (asdf:map-systems (lambda (system) + ;; TODO Perhaps use asdf:primary-system-name + (when (equal asd-pathname + (asdf:system-source-file system)) + (push system result)))) + result)) + (defun system-files (system-designator &key (include-asd t)) "List all the files in a system. Including the .asd file itself." (let ((system (asdf/system:find-system system-designator))) @@ -24,6 +37,13 @@ (mapcar #'asdf/component:component-pathname (asdf/component:sub-components system)))))) +(defun find-all-related-files (system) + "List all files in SYSTEM and in the other systems defined in the same +system definition file." + (remove-duplicates + (alexandria:flatten + (mapcar #'system-files (find-all-related-systems system))))) + (defun system-fasl-directory (system-designator) "Find the directory of a system's fasl files." (asdf:apply-output-translations @@ -108,6 +128,37 @@ (asdf:component-loaded-p (asdf:find-system "breeze")) +#| + +TODO fix loadedp + +Here's a complex but that I never noticed: + +1. The function loadedp calls infer-systems +2. loadedp will crash if it gets a path to a system file that doesn't +contain any loaded system +3. infer-systems returns only system definition files that contains a +system that has not been loaded + +Which means that loadedp crashes when it's called on a file that is +part of a system that no systems from the same system defintion file +was loaded. + +It's subtle... + +The fix is (and/or): +1. Test if I can get away with `(asdf:load-asd ...` +2. add an ignore-error or something similar, because loadedp should +never ever crash... But I would like some warning if an issues arise. +Perhaps add an &optional errorp +3. Maybe find an alternative to asdf/component:sub-component +4. List the systems in the system file and check if any of them are +loaded, if the file is not part of any of these, then the file is +likely not loaded + +|# + + (defun infer-systems (pathname &aux systems) "Given a path (e.g. to a file), infer which systems it might be part of." (let* ((directory (uiop:pathname-directory-pathname pathname)) @@ -134,6 +185,9 @@ This will return false if the file was loaded outside of asdf." (when pathname (loop :for system :in (infer-systems pathname) + ;; BUG if system is a path to an .asd file that has not been + ;; loaded, then sub-components will error (on + ;; asdf/component:component-if-feature). :for components = (asdf/component:sub-components system) :when (typep system 'asdf:system) :do (when-let* ((component-found (member @@ -154,11 +208,14 @@ This will return false if the file was loaded outside of asdf." ;;; Inspecting a system's (transitive) dependencies +;;; See also asdf-dependency-graph (defun system-dependencies (system-designator) (let ((system (asdf/system:find-system system-designator nil))) (remove-if-not #'stringp (asdf:system-depends-on system)))) +;; TODO use https://github.com/gpcz/cl-uniquifier/ to generate the labels? + (defun write-dependecy-graph (root-system &optional (stream t) &aux (system-ids (make-hash-table :test 'equal)) diff --git a/src/breeze.el b/src/breeze.el index e1fe85c6..1d1e1e7b 100644 --- a/src/breeze.el +++ b/src/breeze.el @@ -1,31 +1,23 @@ -;;; package -- breeze integration with emacs ;; -*- lexical-binding: t -*- +;;; package -- breeze integration with emacs ;;; Commentary: ;; -;; Features: -;; - snippets with abbrevs -;; - capture -;; - interactive make-project ;;; Code: - -;;; Scratch section -;; (setf debug-on-error t) -;; (setf debug-on-error nil) - ;;; Requires + (require 'cl-lib) ;;; Logging (defun breeze-debug (string &rest objects) - "Log a meesage in *breeze-debug* buffer." + "Log a meesage in the \" *breeze-debug*\" buffer." (save-current-buffer - (set-buffer (get-buffer-create "*breeze-debug*")) + (set-buffer (get-buffer-create " *breeze-debug*")) (setf buffer-read-only nil) (goto-char (point-max)) (insert @@ -35,186 +27,91 @@ (setf buffer-read-only t))) (defun breeze-message (string &rest objects) - "Log a meesage in *breeze-debug* and *Messages* buffer." + "Log a meesage in both \" *breeze-debug*\" and *Messages* buffers." (apply #'message string objects) (apply #'breeze-debug string objects)) -;;; Variables +;;; Lisp listener state -(defvar breeze-minor-mode-map - (make-sparse-keymap) - "Keymap for breeze-minor-mode") +(defun breeze-sly-connected-p () + "Check if sly loaded, get the list of connections." + (and (fboundp 'sly-connected-p) + (sly-connected-p))) - -;;; Integration with lisp listener - -;; Useful for debugging whether slime or sly is running -;; (process-list) - -;; TODO handle multiple connections -(defun breeze-sly-connection () - "If sly loaded, get the list of connections." - (and (fboundp 'sly-current-connection) - (sly-current-connection))) - -;; TODO this doesn't do the right thing if there are multiple -;; connections e.g. I had 2 connections opened, I closed the one the -;; current buffer was using, when I called breeze-quickfix, it -;; launched a whole new swank. It just happened that it's what I -;; wanted that time, but in general, that's not good. -(defun breeze-slime-connection () +(defun breeze-slime-connected-p () "If slime is loaded, get the list of connections." - (and (fboundp 'slime-current-connection) - (slime-current-connection))) - -(defun breeze-check-if-listener-loaded () - (or (fboundp 'sly) - (fboundp 'slime) - (error "Please load either slime or sly."))) + (and (fboundp 'slime-connected-p) + (slime-connected-p))) -(defun breeze-choose-listener () - (cond - ((and (fboundp 'sly) - (fboundp 'slime)) - (completing-read "Choose a lisp listener to start: " - '("Sly" "SLIME") nil t)) - ((fboundp 'sly) "Sly") - ((fboundp 'slime) "SLIME"))) - -(defun breeze-start-listener () - (let ((listener (breeze-choose-listener))) - (cond - ((string= listener "Sly") (sly)) - ((string= listener "SLIME") (slime)) - (t (error "Unknown listener: %S" listener))))) - -(defun breeze-check-if-listener-connected (&optional errorp) - (or (breeze-sly-connection) - (breeze-slime-connection) - (breeze-start-listener) +(cl-defun breeze-listener-connected-p (&optional (errorp t)) + (or (breeze-sly-connected-p) + (breeze-slime-connected-p) (and errorp (error "Please start either slime or sly.")))) -;; This is used by breeze-eval -(defun breeze-sly-or-not-slime () - "Tries to determine which listener to use, sly or slime?" - ;; This errors if none is loaded - (breeze-check-if-listener-loaded) - ;; This errors if none is connected - (breeze-check-if-listener-connected) - (cond - ((breeze-sly-connection) t) - ((breeze-slime-connection) nil))) +(cl-defun breeze-list-loaded-listeners (&optional (errorp t)) + "Returns a list of loaded listneres (sly or slime)." + (or (remove 'nil (list (and (fboundp 'sly) 'sly) + (and (fboundp 'slime) 'slime))) + (and errorp + (error "Please load either slime or sly.")))) -(defun breeze-%eval (form) - (if (breeze-sly-or-not-slime) (sly-eval form) (slime-eval form))) +(cl-defun breeze-%symbolicate2 (listener &optional suffix) + "Build up a symbol... TODO better docstring." + (cond + (suffix (intern (format "%s-%s" listener suffix))) + ((symbolp listener) listener) + (t (intern listener)))) -(defun breeze-eval (string) - (let ((value (breeze-%eval `(breeze.listener:rpc-eval ,string)))) - (breeze-debug "Breeze: got the value: %S" value) - value)) +(cl-defun breeze-choose-listener (&optional (errorp t)) + (let ((listeners (breeze-list-loaded-listeners errorp))) + (when listeners + (if (= (length listeners) 1) + (cl-first listeners) + (breeze-%listener + (completing-read "Choose a lisp listener to start: " + listeners nil t)))))) -;; (breeze-interactive-eval "1") -;; (breeze-interactive-eval "'(a b c)") -;; (breeze-interactive-eval "t") -;; (breeze-interactive-eval "(not nil)") +(cl-defun breeze-%listener-symbolicate (&optional suffix) + (breeze-%symbolicate2 (breeze-choose-listener) suffix)) -(defun breeze-eval-predicate (string) - (breeze-eval string)) +(cl-defun breeze-%listener-apply (suffix args) + (apply (breeze-%listener-symbolicate suffix) args)) -(defun breeze-eval-list (string) - (breeze-eval string)) +(cl-defun breeze-%listener-funcall (suffix &rest args) + (breeze-%listener-apply suffix args)) - -;;; Prerequisites checks - -(cl-defun breeze-check-if-connected-to-listener (&optional verbosep) - "Make sure that slime or sly is connected, signals an error otherwise." - (if (or - (breeze-sly-connection) - (breeze-slime-connection)) - (progn - (when verbosep - (breeze-message "Slime or Sly already started.")) - t) - ;; TODO Pretty sure we don't want this - (when nil - (progn - (when verbosep - (breeze-message "Starting slime...")) - (slime))))) +(defun breeze-start-listener () + "Start a listener (e.g. calls \"(sly)\" or \"(slime)\")." + (interactive) + (let ((listener (breeze-choose-listener))) + (funcall listener))) -(defun breeze-validate-if-package-exists (package) - "Returns true if the package PACKAGE exists in the inferior-lisp." - (breeze-debug "breeze-validate-if-package-exists %S" package) - (breeze-%eval - `(cl:eval - (cl:and (cl:or (cl:find-package ,(downcase package)) - (cl:find-package ,(upcase package))) - t)))) +(defun breeze-ensure-listener () + (or (breeze-listener-connected-p nil) + (breeze-start-listener))) -(defun breeze-validate-if-breeze-package-exists () - "Returns true if the package \"breeze.utils\" exists in the inferior-lisp." - (breeze-validate-if-package-exists "breeze.utils")) + +;;; Evaluation -;; (breeze-validate-if-breeze-package-exists) +(defun breeze-%eval (form) + (breeze-%listener-funcall "eval" form)) -(defvar breeze-breeze.el load-file-name - "Path to \"breeze.el\".") +(defun breeze-%eval-async (form &optional cont package) + (breeze-%listener-funcall "eval-async" form cont package)) -;; I don't remember why I needed this? maybe I had redefined the -;; defcommand macro. -(defun breeze-reload () - (breeze-eval "(asdf:load-system '#:breeze :force t)")) - -(cl-defun breeze-ensure-breeze () - "Make sure that breeze is loaded in swank or slynk." - (unless (breeze-validate-if-breeze-package-exists) - (breeze-message "Loading breeze's system...") - (breeze-%eval - `(cl:load ,(expand-file-name - (concat - (file-name-directory - breeze-breeze.el) - "/ensure-breeze.lisp"))))) - (breeze-debug "Breeze loaded in inferior-lisp.")) - -;; (breeze-ensure-breeze) +(defun breeze-eval (string) + (let ((value (breeze-%eval `(breeze.listener:rpc-eval ,string)))) + (breeze-debug "Breeze: got the value: %S" value) + value)) -;; See slime--setup-contribs, I named this breeze-init so it _could_ -;; be added to slime-contrib, -;; I haven't tested it yet though. -(cl-defun breeze-init (&optional verbosep) - "Ensure that breeze is initialized correctly on swank's side." - (interactive) - (breeze-check-if-connected-to-listener) - (breeze-ensure-breeze) - (when verbosep - (breeze-message "Breeze initialized."))) - - -(defmacro breeze-with-listener (&rest body) - "Make sure breeze is loaded on the common lisp side, then run BODY." - `(progn - (breeze-init) - ,@body)) - -;; (macroexpand-1 '(breeze-with-listener (print 42))) - -(defun breeze-cl-to-el-list (list) - "Convert NIL to nil and T to t. -Common lisp often returns the symbols capitalized, but emacs -lisp's reader doesn't convert them." - (if (eq list 'NIL) - nil - (mapcar #'(lambda (el) - (cl-case el - (NIL nil) - (T t) - (t el))) - list))) +(defun breeze-eval-async (string &optional cont package) + (breeze-%eval-async + `(breeze.listener:rpc-eval ,string) + cont + package)) ;;; Common lisp driven interactive commands @@ -236,15 +133,14 @@ lisp's reader doesn't convert them." (1- (point-min)) (1- (point-max)))))) -;; TODO extra-args -(defun breeze-command-start (name) +(defun breeze-command-start (name &optional extra-args) "Returns an id" (breeze-debug "Breeze: starting command: %s." name) (let ((id (breeze-eval - (format "(breeze.command:start-command '%s '(%s) %s)" + (format "(breeze.command:start-command '%s '(%s) '%S)" name (breeze-compute-buffer-args) - ;; TODO extra-args + extra-args nil)))) (breeze-debug "Breeze: start-command %S returned %s" name id) id)) @@ -265,6 +161,8 @@ lisp's reader doesn't convert them." (breeze-debug "Breeze: (#%s) request received: %s" id request) request)) + +;; TODO maybe add a "narrow" request type? (defun breeze-command-process-request (request) (pcase (car request) ("choose" @@ -292,26 +190,21 @@ lisp's reader doesn't convert them." (kill-region (1+ point-from) (1+ point-to)) (goto-char (1+ point-from)) (insert replacement-string))) - ("backward-char" - (backward-char (cl-second request)) - ;; Had to do this hack so the cursor is positioned - ;; correctly... probably because of aggressive-indent - (funcall indent-line-function)) ("message" (message "%s" (cl-second request))) ("find-file" (find-file (cl-second request))) - (_ (error "Invalid request: %S" request) ))) + (_ (breeze-debug "Unknown request: %S" request) ))) -;; TODO extra-args -(defun breeze-run-command (name) +(defun breeze-run-command (name &rest extra-args) "Runs a \"breeze command\". TODO Improve this docstring." (interactive) (breeze-debug "breeze-run-command") - (breeze-ensure-breeze) + ;; TODO Do I really want to initialize breeze here? + ;; (breeze-ensure) ;; TODO extra-args - (let ((id (breeze-command-start name))) + (let ((id (breeze-command-start name extra-args))) (condition-case condition (cl-loop ;; guards against infinite loop @@ -341,70 +234,210 @@ lisp's reader doesn't convert them." (breeze-command-cancel id "Elisp condition"))))) -;;; quickfix (similar to code actions in visual studio code) +;;; Dynamically define interactive (cl-driven) commands in emacs + +(defun breeze--remove-suffix (suffix string) + (if (string-suffix-p suffix string) + (cl-subseq string 0 (- (length string) + (length suffix))) + string)) + +(defun breeze-translate-command-symbol (symbol) + (let ((name (symbol-name symbol))) + (cl-destructuring-bind (package command) + (split-string name ":") + (list symbol + (if (string-prefix-p "breeze" name) + (intern (format "breeze-%s" + (breeze--remove-suffix "-command" command))) + ;; TODO maybe add some way to customize this + symbol))))) + +;; TODO this handles only very simplistic cases and it's aleady complex... +;; maybe I should do this translation on the CL side and return something easier to handle??? +(defun breeze-translate-command-lambda-list (lambda-list) + (cl-loop for symbol in lambda-list + for sanitized-symbol = (intern (car (last (split-string (symbol-name symbol) ":")))) + collect sanitized-symbol)) + +(defun breeze-refresh-commands () + "Ask the inferior lisp which commands it has and define +corresponding commands in emacs." + (interactive) + (cl-loop for (symbol cl-lambda-list docstring) in (breeze-eval "(breeze.command:list-all-commands t)") + for (cl-symbol el-symbol) = (breeze-translate-command-symbol symbol) + for el-lambda-list = (breeze-translate-command-lambda-list cl-lambda-list) + for defun = `(cl-defun ,el-symbol (&optional ,@el-lambda-list) + ,docstring + ;; (interactive "" 'lisp-mode 'breeze-minor-mode 'breeze-major-mode) + (interactive) + (breeze-run-command ,(symbol-name cl-symbol) ,@el-lambda-list)) + do + ;; (breeze-debug "%S" defun) + (eval defun))) + + +;;; "Autoload" + +;; TODO breeze-not-initialized-hook + +(defun breeze--stub (name) + (warn "Breeze is not loaded") + (and + (breeze-list-loaded-listeners) + (breeze-listener-connected-p) + (breeze-validate-if-breeze-package-exists) + (breeze-refresh-commands))) (defun breeze-quickfix () - "Choose from a list of commands applicable to the current context." (interactive) - (breeze-run-command "breeze.refactor:quickfix")) + (breeze--stub "quickfix")) -(defun breeze-insert-defpackage () - "Choose a command from a list of applicable to the current context." - (interactive) - (breeze-run-command "breeze.refactor:insert-defpackage")) + +;;; Initializations -(defun breeze-eval-defun () - "Evaluate current top-level form." - (interactive) - (breeze-run-command "breeze.listener:interactive-eval-command")) +(defun breeze-validate-if-package-exists (package) + "Returns true if the package PACKAGE exists in the inferior lisp." + (breeze-debug "breeze-validate-if-package-exists %S" package) + (breeze-%eval + `(cl:eval + (cl:and (cl:or (cl:find-package ,(downcase package)) + (cl:find-package ,(upcase package))) + t)))) -;; TODO narrow-to-defun (maybe clone the buffer too?) -- "focus to defun"... +(defun breeze-validate-if-breeze-package-exists () + "Returns true if the package \"breeze.utils\" exists in the +inferior lisp." + (breeze-validate-if-package-exists "breeze.utils")) - -;;; code evaluation -;; -;; TODO This will not work for a while, I want to wrap both sly and -;; slime on emacs side and call a "breeze-eval" on lisp side. Then I'm -;; not sure I want this exact feature... -;; +(defvar breeze-breeze.el load-file-name + "Path to \"breeze.el\".") -(when nil - (defun breeze-get-recently-evaluated-forms () - "Get recently evaluated forms from the server." - (cl-destructuring-bind (output value) - (slime-eval `(swank:eval-and-grab-output - "(breeze.listener:get-recent-interactively-evaluated-forms)")) - (split-string output "\n")))) - -(when nil - (defun breeze-reevaluate-form () - (interactive) - (let ((form (completing-read "Choose recently evaluated form: " - (breeze-get-recently-evaluated-forms)))) - (when form - (slime-interactive-eval form))))) +(defun breeze-relative-path (&rest components) + (expand-file-name + (apply 'file-name-concat + (file-name-directory breeze-breeze.el) + ".." + components))) + +;; TODO this doesn't work on "remote systems" +(cl-defun breeze-load (&optional cont) + "Load breeze into the inferior system." + (breeze-%eval-async + `(cl:load ,(breeze-relative-path "src/ensure-breeze.lisp")) + cont)) + +(cl-defun breeze-ensure (&optional callback) + "Make sure that breeze is loaded in the inferior lisp." + (if (breeze-validate-if-breeze-package-exists) + (when callback (funcall callback)) + (breeze-message "Loading breeze's system asynchronously...") + (breeze-load + (lambda (&rest _) + (breeze-message "Breeze loaded in inferior-lisp.") + (breeze-refresh-commands) + (when callback (funcall callback)))))) + + +;; See slime--setup-contribs, I named this breeze-init so it _could_ +;; be added to slime-contrib, +(cl-defun breeze-init () + "Initialize breeze." + (interactive) + (breeze-ensure-listener) + (breeze-ensure) + (breeze-debug "Breeze initialized (might still be loading in the inferior lisp.")) -;;; project scaffolding +;;; Listener Hooks + +;; TODO This is experimental! I mean... more than the rest xD +(defun breeze-%%%setup-hooks (listener) + (when (eq 'slime listener) + (cl-loop for hook in '(slime-connected-hook + slime-inferior-process-start-hook + slime-net-process-close-hooks + slime-cycle-connections-hook + slime-connected-hook + slime-event-hooks) + do (add-hook hook (lambda (&rest args) (breeze-debug "%S: %S" hook args) nil)) ))) + +(defun breeze-connected-hook-function () + (breeze-ensure)) + +(defun breeze-enable-connected-hook () + "Configure a hook to initialize breeze when connecting to sly or slime." + (interactive) + (add-hook (breeze-%listener-symbolicate "connected-hook") + 'breeze-connected-hook-function)) -(defun breeze-scaffold-project () - "Create a project using quickproject." +(defun breeze-disable-connected-hook () (interactive) - (breeze-run-command "breeze.project:scaffold-project")) + "Remove the hook to initialize breeze when connecting to sly or slime." + (remove-hook (breeze-%listener-symbolicate "connected-hook") + 'breeze-connected-hook-function)) -;;; capture +;;; Hooks for flymake + +(defun breeze-lint (callback) + (breeze-ensure + (lambda () + (breeze-eval-async + (format "(breeze.analysis:lint %s)" + (breeze-compute-buffer-args)) + callback))) + nil) + +(defun breeze-flymake (report-fn &rest args) + (breeze-debug "flymake: %S" args) + (if (and (breeze-listener-connected-p) + ;; TODO breeze-ready-p + ) + (let ((buffer (current-buffer))) + (breeze-lint (lambda (cl-diagnostics) + (funcall report-fn + (cl-loop for (beg end type text) in cl-diagnostics + collect (flymake-make-diagnostic + ;; Locus + buffer + (1+ beg) (1+ end) + type + text)))))) + ;; Not connected, so we can't call breeze's linter. + (funcall report-fn nil))) + +(defun breeze-enable-flymake-backend () + (interactive) + (add-hook 'flymake-diagnostic-functions 'breeze-flymake nil t)) + +(defun breeze-disable-flymake-backend () + (interactive) + (remove-hook 'flymake-diagnostic-functions 'breeze-flymake nil)) -(defun breeze-capture () - "Create a file ready to code in." +;; TODO assumes slime +;; TODO this doesn't work well at all +(defun breeze-next-note () (interactive) - (breeze-run-command "breeze.capture:capture")) + (let ((slime-note (slime-find-next-note))) + (if slime-note + (slime-next-note) + (flymake-goto-next-error)))) + +;; TODO assumes slime +(defun breeze-previous-note () + (interactive) + (let ((slime-note (slime-find-previous-note))) + (if slime-note + (slime-previous-note) + (flymake-goto-prev-error)))) ;;; WIP Alternate files (this is currently very brittle, but it should ;;; work for most of my projects). ;;; ;;; TODO Better docstrings +;;; TODO move this logic to the inferior lisp! (defun breeze--candidate-aternate-directories () "Generate a list of existing alternate directories." @@ -475,6 +508,10 @@ lisp's reader doesn't convert them." ;;; minor mode +(defvar breeze-minor-mode-map + (make-sparse-keymap) + "Keymap for breeze-minor-mode") + (define-minor-mode breeze-minor-mode "Breeze mimor mode." :lighter " brz" @@ -484,10 +521,15 @@ lisp's reader doesn't convert them." ;; (define-key breeze-minor-mode-map (kbd "C-c C-,") 'breeze-insert) ;; Analogous to org-goto -(define-key breeze-minor-mode-map (kbd "C-c C-j") #'imenu) +(keymap-set breeze-minor-mode-map "C-c C-j" #'imenu) ;; Analogous to Visual Studio Code's "quickfix" -(define-key breeze-minor-mode-map (kbd "C-.") #'breeze-quickfix) +(keymap-set breeze-minor-mode-map "C-." #'breeze-quickfix) + +;; TODO M-n M-p https://www.gnu.org/software/emacs/manual/html_node/flymake/Finding-diagnostics.html + +(keymap-set breeze-minor-mode-map "M-p" #'breeze-previous-note) +(keymap-set breeze-minor-mode-map "M-n" #'breeze-next-note) ;; Disabled for now ;; eval keymap - because we might want to keep an history @@ -507,10 +549,27 @@ lisp's reader doesn't convert them." (interactive) (breeze-minor-mode -1)) -;; (add-hook 'breeze-minor-mode-hook 'breeze-init) -;; TODO This should be in the users' config -;; (add-hook 'slime-lisp-minor-mode-hook 'breeze-init) -;; TODO This should be in the users' config +(defun breeze-minor-mode-enable-flymake-mode () + "Configure a hook to enable flymake-mode when breeze-minor mode is enabled" + (interactive) + (add-hook 'breeze-minor-mode-hook 'flymake-mode) + (add-hook 'breeze-minor-mode-hook 'breeze-enable-flymake-backend)) + +(defun breeze-minor-mode-disable-flymake-mode () + "Configure a hook to enable flymake-mode when breeze-minor mode is enabled" + (interactive) + (remove-hook 'breeze-minor-mode-hook 'flymake-mode) + (remove-hook 'breeze-minor-mode-hook 'breeze-enable-flymake-backend)) + +(defun breeze-enable-minor-mode-hook () + "Configure a hook to enable breeze-minor-mode in lisp-mode." + (interactive) + (add-hook 'lisp-mode-hook #'breeze-minor-mode)) + +(defun breeze-disable-minor-mode-hook () + "Configure a hook to enable breeze-minor-mode in lisp-mode." + (interactive) + (remove-hook 'lisp-mode-hook #'breeze-minor-mode)) ;;; major mode @@ -518,20 +577,10 @@ lisp's reader doesn't convert them." (define-derived-mode breeze-major-mode prog-mode "BRZ") -(define-key breeze-major-mode-map (kbd "C-.") #'breeze-quickfix) - -(define-key breeze-major-mode-map (kbd "C-c C-c") #'breeze-eval-defun) +(keymap-set breeze-major-mode-map "C-." #'breeze-quickfix) +(keymap-set breeze-major-mode-map "C-c C-c" #'breeze-eval-defun) -;; TODO define-key: This is a legacy function; see ‘keymap-set’ for -;; the recommended function to use instead. - - -(defun breeze () - "Initialize breeze." - (interactive) - (breeze-init t)) - (provide 'breeze) ;;; breeze.el ends here diff --git a/src/breeze.lisp b/src/breeze.lisp new file mode 100644 index 00000000..8a5310d2 --- /dev/null +++ b/src/breeze.lisp @@ -0,0 +1,16 @@ + +(uiop:define-package #:breeze + (:documentation "The breeze package meant for the end-user.") + (:use #:cl) + (:use-reexport) + (:import-from #:breeze.command + #:define-command) + (:export #:define-command)) + +(in-package #:breeze) + +#++ +(let ((package #:breeze.command) + (symbols '(#:define-command + #:read-string-then-insert))) + (loop import then export)) diff --git a/src/command.lisp b/src/command.lisp index 445448b0..af911312 100644 --- a/src/command.lisp +++ b/src/command.lisp @@ -1,7 +1,7 @@ (cl:in-package #:common-lisp-user) (uiop:define-package #:breeze.command - (:documentation "Interactive commands' core") + (:documentation "Interactive commands' core") (:use :cl :breeze.logging) (:import-from #:alexandria #:symbolicate @@ -9,12 +9,6 @@ #:if-let #:lastcar #:when-let*) - #++ - (:import-from #:breeze.reader - #:parse-string) - #++ - (:import-from #:breeze.syntax-tree - #:find-path-to-node) (:import-from #:breeze.utils #:before-last) (:export @@ -26,18 +20,12 @@ #:context* #:context-get #:context-set - #:context-buffer-string - #:context-buffer-string* - #:context-buffer-name - #:context-buffer-name* - #:context-buffer-file-name - #:context-buffer-file-name* - #:context-point - #:context-point* - #:context-point-min - #:context-point-min* - #:context-point-max - #:context-point-max* + #:buffer-string + #:buffer-name + #:buffer-file-name + #:point + #:point-min + #:point-max ;; Basic composables commands #:insert #:read-string @@ -46,7 +34,6 @@ #:insert-at #:insert-at-saving-excursion #:replace-region - #:backward-char #:message #:find-file #:ask-y-or-n-p @@ -54,6 +41,7 @@ #:return-from-command #:define-command #:commandp + #:list-all-commands ;; Utilities to add very useful information into the context #:augment-context-by-parsing-the-buffer ;; Keys in the *context* hash-table @@ -112,7 +100,7 @@ actor)) ;; TODO gabage collect the *actors* that are done (when?) -;; TODO How to dectect if something went wrong? +;; TODO How to detect if something went wrong? (defun clear-actors () "Forget all the actors" @@ -223,8 +211,6 @@ (defmethod send-out ((command command-handler) value) (%send (channel-out command) value)) -;; No, I won't support multiple client/command at the same time, for -;; now™. (defvar *command* nil "The command that is currently being executed.") @@ -241,8 +227,6 @@ (send-out *command* `(,request ,@data))) -;; TODO rename cancel -> stop - (defun stop-actor (actor) (when-let* ((thread (thread actor))) (handler-case @@ -260,11 +244,12 @@ (bt:destroy-thread thread))) ;; This is signaled when interrupting a thread fails because the ;; thread is not alive. (p.s. on sbcl, both bt:interrupt-thread - ;; and bt:destroy-thread ends up interrupting th thread) + ;; and bt:destroy-thread ends up interrupting the thread) #+sbcl (sb-thread:interrupt-thread-error (condition) (declare (ignore condition)))))) +;; TODO rename cancel -> stop (defun cancel-command (id &optional reason) "Cancel a command." (let ((actor (find-actor id :errorp t))) @@ -423,9 +408,6 @@ (id command))) -;; (run-command command nil) - - ;; TODO Maybe rename ARGUMENTS to RESPONSE? (defun continue-command (id &rest response) "Continue procressing *command*." @@ -454,94 +436,71 @@ (context *command*) (error "*command* is nil"))) -(defun context-get (context key) - "Get the value of KEY CONTEXT." - (gethash key context)) +(define-condition request () + ((what :initarg :what :reader what))) + +(defun request (what) + (catch 'answer + (signal 'request :what what))) + +(defun answer (value) + (throw 'answer (values value t))) (defun context-set (context key value) "Set KEY to VALUE in CONTEXT." (setf (gethash key context) value)) -;; TODO remove useless prefix "context-" - -(defun context-buffer-string (context) +(defun context-get (context key) + "Get the value of KEY CONTEXT." + (multiple-value-bind (value presentp) + (gethash key context) + (if presentp + value + (multiple-value-bind (value answeredp) + (request key) + (when answeredp + (context-set context key value)) + value)))) + +(defun buffer-string (&optional (context (context*))) "Get the \"buffer-string\" from the CONTEXT. The buffer-string is the content of the buffer. It can be null." (context-get context 'buffer-string)) -(defun context-buffer-string* () - "Get the \"buffer-string\" from the *command*'s context. -The buffer-string is the content of the buffer. -It can be null." - (context-get (context*) 'buffer-string)) - -(defun context-buffer-name (context) +(defun buffer-name (&optional (context (context*))) "Get the \"buffer-name\" from the CONTEXT. The buffer-name is the name of the buffer. It can be null." (context-get context 'buffer-name)) -(defun context-buffer-name* () - "Get the \"buffer-name\" from the *command*'s context. -The buffer-name is the name of the buffer. -It can be null." - (context-get (context*) 'buffer-name)) - -(defun context-buffer-file-name (context) +(defun buffer-file-name (&optional (context (context*))) "Get the \"buffer-file-name\" the CONTEXT. The buffer-file-name is the name of the file that the buffer is visiting. It can be null." (context-get context 'buffer-file-name)) -(defun context-buffer-file-name* () - "Get the \"buffer-file-name\" from the *command*'s context. -The buffer-file-name is the name of the file that the buffer is -visiting. -It can be null." - (context-get (context*) 'buffer-file-name)) - -(defun context-point (context) +(defun point (&optional (context (context*))) "Get the \"point\" from the CONTEXT. The point is the position of the cursor. It can be null." (context-get context 'point)) -(defun context-point* () - "Get the \"point\" from the *command*'s context. -The point is the position of the cursor. -It can be null." - (context-get (context*) 'point)) - -(defun context-point-min (context) +(defun point-min (&optional (context (context*))) "Get the \"point-min\" from the CONTEXT. The point-min is the position of the beginning of buffer-string. See \"narrowing\" in Emacs. It can be null." (context-get context 'point-min)) -(defun context-point-min* () - "Get the \"point-min\" from the *command*'s context. -The point-min is the position of the beginning of buffer-string. -See \"narrowing\" in Emacs. -It can be null." - (context-get (context*) 'point-min)) - -(defun context-point-max (context) +(defun point-max (&optional (context (context*))) "Get the \"point-max\" from the CONTEXT. The point-max is the position of the end of buffer-string. See \"narrowing\" in Emacs. It can be null." (context-get context 'point-max)) -(defun context-point-max* () - "Get the \"point-max\" from the *command*'s context. -The point-max is the position of the end of buffer-string. -See \"narrowing\" in Emacs. -It can be null." - (context-get (context*) 'point-max)) - ;;; Basic commands, to be composed @@ -610,10 +569,6 @@ to non-nil to keep the current position." position-to replacement-string)) -(defun backward-char (&optional n) - "Send a message to the editor to move backward." - (send "backward-char" n)) - (defun message (control-string &rest format-arguments) "Send a message to the editor to ask it to show a message to the user. This function pass its arguments to cl:format and sends the @@ -640,6 +595,23 @@ resulting string to the editor." (defun commandp (symbol) (get symbol 'breeze.command::commandp)) + +(defun list-all-commands (&optional with-details-p) + (loop + :for package :in (list-all-packages) + #++ (breeze.xref:find-packages-by-prefix "breeze.") + :append (loop + :for symbol :being :each :external-symbol :of package + :for lambda-list-or-t = (commandp symbol) + :when lambda-list-or-t + :collect (if with-details-p + (list symbol (if (eq t lambda-list-or-t) + nil + lambda-list-or-t) + (documentation symbol 'function)) + symbol)))) + + (defmacro define-command (name lambda-list &body body) "Macro to define command with the basic context. @@ -665,33 +637,30 @@ Example: (progn ,@remaining-forms) (send "done")))) ;; Add a flag into the symbol's plist - (setf (get ',name 'commandp) t)))) + (setf (get ',name 'commandp) ',(or lambda-list t))))) ;;; Utilities to get more context +;; TODO It should be easier to test with the request/answer stuff? (defun parse-buffer (context) - #++ - (let* ((buffer-string (context-buffer-string context)) - (code (parse-string buffer-string))) - (breeze.reader:forms code))) + (breeze.lossless-reader:parse (buffer-string context))) -;; TODO Add lots of error-handling... (defun augment-context-by-parsing-the-buffer (context) - (let ((nodes (parse-buffer context))) - (if nodes - (let* (;; Find the node "at point" - (path (find-path-to-node (context-point context) nodes)) - ;; Find the top-level form "at point" - (outer-node (caar path)) - ;; Find the innermost form "at point" - (inner-node (car (lastcar path))) - (inner-node-index (cdr (lastcar path))) - ;; Find the innermost form's parent - (parent-node (car (before-last path)))) - #. `(progn ,@(loop :for key in '(nodes path outer-node - inner-node inner-node-index parent-node) - :collect - `(context-set context ',key ,key))) - t) - nil))) + (let ((parse-result (parse-buffer context))) + ;; TODO re-implement those against the new parse tree + #++ + (let* (;; Find the node "at point" + (path (find-path-to-node (point context) nodes)) + ;; Find the top-level form "at point" + (outer-node (caar path)) + ;; Find the innermost form "at point" + (inner-node (car (lastcar path))) + (inner-node-index (cdr (lastcar path))) + ;; Find the innermost form's parent + (parent-node (car (before-last path)))) + #. `(progn ,@(loop :for key in '(nodes path outer-node + inner-node inner-node-index parent-node) + :collect + `(context-set context ',key ,key)))) + parse-result)) diff --git a/src/configuration.lisp b/src/configuration.lisp index 1fca1008..0e0d9232 100644 --- a/src/configuration.lisp +++ b/src/configuration.lisp @@ -1,6 +1,3 @@ -;;; TODO This should come with its own system, so that people can -;;; configure breeze in their init file, without loading the whole -;;; breeze system. (defpackage #:breeze.configuration (:documentation "Breeze's configuration") diff --git a/src/documentation.lisp b/src/documentation.lisp index c1769d62..5a3e3588 100644 --- a/src/documentation.lisp +++ b/src/documentation.lisp @@ -114,6 +114,7 @@ (list wrapper loop) loop)))))))) +;; TODO turn this into a test #+nil (map-external-symbol (sym (find-package :br)) (boundp sym) @@ -121,23 +122,16 @@ print :collect (print sym)) +;; TODO turn this into a test #+nil (map-external-symbol - (symbol (find-package :br)) - (boundp symbol) - (:h3 "Special variables") - :dl - :do - (:dt (symbol-name symbol)) - (:dd (documentation symbol 'variable))) - - -(defun render-markdown (pathname) - "Read a markdown file and render it in spinneret's *html* stream." - (let ((3bmd-tables:*tables* t)) - (3bmd:parse-and-print-to-stream - (breeze-relative-pathname pathname) - spinneret:*html*))) + (symbol (find-package :br)) + (boundp symbol) + (:h3 "Special variables") + :dl + :do + (:dt (symbol-name symbol)) + (:dd (documentation symbol 'variable))) ;; TODO @@ -164,27 +158,19 @@ - [ ] Structures - [ ] Type definitions (I'm not sure this one can be done with introspection alone). |# + +(defun find-breeze-packages () + (remove-if (lambda (package) + (position #\. (package-name package) :start (length #1="breeze."))) + (breeze.xref:find-packages-by-prefix #1#))) + (defun render-reference () (spinneret:with-html (let ((packages (sort - (breeze.xref:find-packages-by-regex "^breeze\\.[^.]+$") + (find-breeze-packages) #'string< :key #'package-name))) - #+nil - (progn - (:h1 "Packages' documentation") - (loop - :for package :in packages - :for package-name = (string-downcase (package-name package)) - :for docfile = (breeze-relative-pathname - (format nil "docs/~a.md" package-name)) - :do - (if (probe-file docfile) - (progn - (:h2 (:a :id package-name package-name)) - (render-markdown docfile)) - (warn "Could not find \"~a\"." docfile)))) (:h1 (:a :id "reference" "Reference")) ;; Package index (:dl @@ -199,8 +185,6 @@ (loop :for package :in packages :for package-name = (string-downcase (package-name package)) - :for docfile = (breeze-relative-pathname - (format nil "docs/~a.md" package-name)) :do (macrolet ((gen (title predicate-body @@ -225,31 +209,28 @@ (defun generate-documentation-to-stream (stream) (let ((spinneret:*html* stream)) - (let ( - (spinneret:*suppress-inserted-spaces* t) + (let ((spinneret:*suppress-inserted-spaces* t) (spinneret:*html-style* :tree) (*print-pretty* nil)) (spinneret:with-html - (:doctype) + (:doctype) (:html (:head - (:title "Breeze") + (:title "Reference") (:link :rel "stylesheet" :href "style.css")) (:body - (:ol - (:li (:a :href "#readme" "Breeze")) - (:li (:a :href "#emacs" "Emacs integration")) - (:li (:a :href "#reference" "Reference"))) - (render-markdown "README.md") - (render-markdown "docs/emacs.md") (render-reference))))))) (defun generate-documentation () - (let ((index (breeze-relative-pathname "docs/index.html"))) + (let* ((root (breeze-relative-pathname "docs/")) + (index (merge-pathnames "reference.html" root)) + #+(and sbcl windows) + (sb-impl::*default-external-format* :utf-8)) + (ensure-directories-exist root) (with-output-to-file (output index :if-exists :supersede :if-does-not-exist :create) (generate-documentation-to-stream output) - (format t "~%breeze.documentation: ~s written.~%" index)))) + (format *trace-output* "~%breeze.documentation: ~s written.~%" index)))) diff --git a/src/egraph.lisp b/src/egraph.lisp new file mode 100644 index 00000000..4ac6d3a7 --- /dev/null +++ b/src/egraph.lisp @@ -0,0 +1,797 @@ +(uiop:define-package #:breeze.egraph + (:documentation "Equivalence graphs to compactly represent lots of code. + +- egraphs ≅ set of eclasses +- eclasses ≅ set of enodes +- enode ≅ an operator + operands eclasses (not enodes)") + (:use #:cl) + (:use-reexport #:breeze.pattern) + ;; Eclass + (:export + #:make-eclass + #:id + #:enodes + #:parents) + ;; Egraph + (:export + #:egraph + #:make-egraph + #:enode-eclasses + #:eclasses + #:union-find + #:eclasses + #:pending + #:input-eclasses) + (:export + #:eclass + #:eclass-id + #:eclass-find + #:canonicalize + #:egraph-add-enode + #:merge-eclass + #:rebuild) + (:export + #:root-node-p + #:root-eclass-p + #:root-eclasses + #:enode< + #:smallest-enodes) + (:export + #:add-form + #:add-input) + (:export + #:match-rewrite + #:apply-rewrite)) + +(in-package #:breeze.egraph) + + +;;; Disjoint-sets data structure + +(defun make-disjoint-sets (&optional number-of-sets) + "Create a set of sets represented as an array. + +Examples: +(make-disjoint-sets) +=> #() + +(make-disjoint-sets 10) +;; => #(0 1 2 3 4 5 6 7 8 9) +" + (let ((sets (make-array (list (or number-of-sets 0)) + :element-type 'integer + :adjustable t + :fill-pointer t))) + (when number-of-sets + (loop :for i :below number-of-sets + :do (setf (aref sets i) i))) + sets)) + +(defun disjoint-sets-add (sets) + "Add a new item into its own disjoint set. Return a new id. + +Example: + +(let ((id (disjoint-sets-add sets))) + ;; SETS is modified + ...) +" + (let ((new-id (length sets))) + (vector-push-extend new-id sets) + new-id)) + +(defun disjoint-sets-find (sets id) + "Find the id of the set representative (the root). + +Example: + +(disjoint-sets-find sets 5) +" + (let ((parent (aref sets id))) + (if (= id parent) + ;; If "id" is the root, just return it. + id + (let ((root (disjoint-sets-find sets parent))) + ;; TODO Don't do the path compression here... I feel like + ;; it's going to add unecesary step to the egraph's hot + ;; loop. + ;; Path compression: point directly to the root if it's not + ;; already the case. + (when (/= root parent) + (setf (aref sets id) root)) + root)))) + +(defun disjoint-sets-union (sets id1 id2) + "Merge two disjoint sets. Return the set representative (the root) + +Example: + +(disjoint-sets-union sets 1 2) +=> 4 ; SETS is modified. +" + (let ((root1 (disjoint-sets-find sets id1)) + (root2 (disjoint-sets-find sets id2))) + (setf (aref sets root2) root1))) + +(defun disjoint-sets-same-set-p (sets id1 id2) + "Test if 2 items are in the same set. + +Example: + +(disjoint-sets-same-set-p sets 1 2) +=> T or NIL" + (= (disjoint-sets-find sets id1) + (disjoint-sets-find sets id2))) + + + +(defun make-enode-map () + "Make an empty map from enodes to eclass ids" + (make-hash-table :test 'equalp)) + +(defclass eclass () + ((id + :initform (error "eclass: ID must be specified.") + :initarg :id + :accessor id + :documentation "The id (integer) of the equivalenceclass.") + (enodes + :initform (make-array '(0) :adjustable t :fill-pointer t) + :initarg :enodes + :accessor enodes + :documentation "The set of enodes that are equivalent.") + (parents + :initform (make-enode-map) + :initarg :parents + :accessor parents + :documentation + "Back-pointer to the parent enodes. Used for repairing the egraph's +invariants. +Keys are parent enodes +Values are the parent enode's eclass-id +")) + (:documentation "An equivalence class")) + +(defun make-eclass (id enodes &optional parents) + (make-instance + 'eclass + :id id + :enodes (make-array (list (length enodes)) + :adjustable t + :fill-pointer t + :initial-contents enodes) + :parents (or parents (make-enode-map)))) + +(defun eclass-add-enode (eclass enode) + "Add an ENODE to an ECLASS." + (vector-push-extend enode (enodes eclass))) + +(defun add-parent (eclass parent-enode parent-eclass-id) + "Add the PARENT-ENODE to ECLASS's parents" + (setf (gethash parent-enode (parents eclass)) parent-eclass-id)) + +(defmethod print-object ((eclass eclass) stream) + "Print ECLASS to STREAM." + (format stream "#" (id eclass) (enodes eclass)) + #++ (print-unreadable-object + (eclass stream :type t :identity nil) + (format stream "~3d" (id eclass)) + (format stream " ~w" (enodes eclass)) + ;; (format stream "~{~w~}" (enodes eclass)) + #++ + (if (= 1 (length (enodes eclass))) + (format stream "~d (1 enode: ~s, ~d parent enodes)" + (id eclass) + (aref (enodes eclass) 0) + (hash-table-count (parents eclass))) + (format stream "~d (~d enodes, ~d parent enodes)" + (id eclass) + (length (enodes eclass)) + (hash-table-count (parents eclass)))))) + + + +(defclass egraph () + ((enode-eclasses ;; enode -> eclass id + :initform (make-hash-table :test 'equalp) + :accessor enode-eclasses + :documentation "A mapping from enode objects to eclass IDs.") + (eclasses + :initform (make-hash-table) + :initarg :eclasses + :accessor eclasses + :documentation "Eclasses by their IDs.") + (union-find + :initform (make-disjoint-sets) + :initarg :union-find + :accessor union-find + :documentation + "A union-find data structure, that keeps track of equivalences between +eclasses.") + (pending + :initform (list) + :initarg :pending + :accessor pending + :documentation "A list of pending eclass ids to fix their invariants.") + (input-eclasses + :initform (list) + :accessor input-eclasses + :documentation "A convenient list of eclass ids considered as \"inputs\".")) + (:documentation "An equivalence graph")) + + +(defmethod print-object ((egraph egraph) stream) + "Print EGRAPH to STREAM." + (print-unreadable-object + (egraph stream :type t :identity nil) + (format stream "~d e-nodes across ~d e-classes, ~d pending repairs" + (hash-table-count (enode-eclasses egraph)) + (hash-table-count (eclasses egraph)) + ;; (length (union-find egraph)) + (length (pending egraph))))) + +(defun make-egraph () + "Create an empty egraph (equivalence graph)." + (make-instance 'egraph)) + + + +(defun eclass (egraph eclass-id) + "Get an eclass in EGRAPH from its ECLASS-ID." + (gethash eclass-id (eclasses egraph))) + +(defun (setf eclass) (eclass egraph eclass-id) + "Add an ECLASS to EGRAPH, indexed by its ECLASS-ID." + (setf (gethash eclass-id (eclasses egraph)) eclass)) + +(defun eclass-id (egraph enode) + "Get an EGRAPH's ENODE's eclass-id." + (gethash enode (enode-eclasses egraph))) + +(defun (setf eclass-id) (eclass-id egraph enode) + "Set an EGRAPH's ENODE's ECLASS-ID." + (setf (gethash enode (enode-eclasses egraph)) eclass-id)) + +(defun eclass-find (egraph eclass-id) + "Find the canonical id for EGRAPH's eclass ECLASS-ID." + (disjoint-sets-find (union-find egraph) eclass-id)) + +(defmethod enodes ((egraph egraph)) + "Get all the enodes in EGRAPH as a list." + (alexandria:hash-table-keys (enode-eclasses egraph))) + +(defun enode-parents (egraph enode) + "Returns a list of parent enodes." + (alexandria:hash-table-keys + (parents + (eclass egraph (eclass-id egraph enode))))) + +(defun root-eclass-p (eclass) + "Is eclass a root eclass?" + (zerop (hash-table-count (parents eclass)))) + +(defun root-enode-p (egraph enode) + (zerop + (hash-table-count + (parents + (eclass egraph (eclass-id egraph enode)))))) + + + +(defun canonicalize (egraph enode) + "Return ENODE with all its children's eclass-id replaced by the +canonical (representative) eclass-id. + +The first value is the original ENODE if it was already canonical, or +a completely new enode if it wasn't. + +The second value is NIL iif ENODE was already canonical." + (loop + :with changed + ;; TODO there's probably a way to postpone copy-seq until we know + ;; it's really needed + :with new-enode = (copy-seq enode) + :for i :from 1 :below (length enode) + :for eclass-id = (aref enode i) + :for canonical-eclass-id = (eclass-find egraph eclass-id) + :unless (= eclass-id canonical-eclass-id) + :do (setf changed t + (aref new-enode i) canonical-eclass-id) + :finally (return (values (if changed + new-enode + enode) + changed)))) + + + +(defun enode-add-parent (egraph parent-eclass-id enode) + "Go through the ENODE's children eclass and add PARENT-ECLASS-ID as its parent." + ;; For each children of ENODE; add the new eclass as a parent. + (when (vectorp enode) + (loop + :for i :from 1 :below (length enode) + :for child-eclass-id := (aref enode i) + #++ (let ((id (aref enode i))) + (if (typep id 'eclass) (id id) id)) + :for child-eclass := (eclass egraph child-eclass-id) + :do (add-parent child-eclass enode parent-eclass-id)))) + +(defun egraph-add-enode (egraph enode) + "Add ENODE to EGRAPH, creating a new e-class if necessary. Returns the +eclass-id (and a second value T, if the eclass was just created)." + (or + ;; Do nothing if the enode already exists in the egraph + (when (typep enode 'eclass) + (values (id enode) t)) + (alexandria:when-let ((eclass-id (eclass-id egraph enode))) + (values eclass-id t)) + (let* (;; Allocate a new eclass-id + (id (disjoint-sets-add (union-find egraph))) + ;; Create a new eclass with that id + (eclass (make-eclass id (list enode)))) + ;; Set the ENODE's eclass-id too + (setf (eclass-id egraph enode) id) + ;; Add the new eclass into the egraph + (setf (eclass egraph id) eclass) + ;; For each children of ENODE; add the new eclass as a parent. + (enode-add-parent egraph id enode) + ;; Return the new eclass-id + (values id nil)))) + +(defun merge-eclass (egraph id1 id2) + "Merge eclasses represented by ID1 and ID2. +This breaks the invariant of the egraph, rebuild must be called +aftewards to restore them." + (let ((canonical-id1 (eclass-find egraph id1)) + (canonical-id2 (eclass-find egraph id2))) + (if (= canonical-id1 canonical-id2) + canonical-id1 + (let ((new-canonical-id + (disjoint-sets-union (union-find egraph) id1 id2))) + (push new-canonical-id (pending egraph)) + (let* ((eclass1 (eclass egraph id1)) + (eclass2 (eclass egraph id2)) + (parents1 (parents eclass1)) + (parents2 (parents eclass2))) + (unless (eq parents1 parents2) + ;; now they share the same table + (setf (parents eclass2) parents1) + ;; TODO add parents2 to eclass1's parents + (maphash (lambda (parent-enode parent-eclass-id) + (add-parent eclass1 parent-enode parent-eclass-id)) + parents2))) + new-canonical-id)))) + +(defmethod repair-parent-enodes (egraph eclass) + "Make sure each enodes is canonical and points to a canonical +eclass." + (loop + :for parent-enode + :being :the :hash-key :of (parents eclass) + :using (hash-value parent-eclass-id) + :for new-enode = (canonicalize egraph parent-enode) + ;; Make sure the eclass ids contained in the enode are all + ;; canonical eclass ids + :unless (eq new-enode parent-enode) + :do (remhash parent-enode (enode-eclasses egraph)) + :do + ;; Make sure the enode points to a canonical eclass id + (setf (eclass-id egraph new-enode) + (eclass-find egraph parent-eclass-id)))) + +(defun repair-congruence (egraph eclass) + "Restore the \"congruence\" invariant. + +If you canonicalize an enode and it has a new eclass, it means that +the new eclass is equivalent to the old one, and they must be merged. +" + (loop + :with new-parent-enodes = (make-enode-map) + :for parent-enode + :being :the :hash-key :of (parents eclass) + :using (hash-value parent-eclass-id) + :for new-enode = (canonicalize egraph parent-enode) + :for equivalent-eclass-id = (gethash new-enode new-parent-enodes) + :do + (when equivalent-eclass-id + (merge-eclass egraph + parent-eclass-id + equivalent-eclass-id)) + (setf (gethash new-enode new-parent-enodes) + (eclass-find egraph parent-eclass-id)) + :finally (setf (parents eclass) new-parent-enodes))) + +(defun repair (egraph eclass-id) + "Repair 1 eclass after it was merged with another." + (let ((eclass (eclass egraph eclass-id))) + (repair-parent-enodes egraph eclass) + (repair-congruence egraph eclass))) + +(defun to-set (sequence) + "Utility to transform SEQUENCE to a hash-table where each key is also +the value. +It is used to de-duplicate SEQUENCE's elements." + (let ((set (make-hash-table))) + (map nil (lambda (element) + (setf (gethash element set) element)) + sequence) + set)) + +(defun rebuild (egraph) + "Restore all EGRAPH's invariants. +It must be called after merging eclasses, but it is possible to do +many merges in a batch and only call rebuild once afterwards." + (loop + :while (pending egraph) + :do + (let ((todo (to-set (mapcar (lambda (eclass-id) + (eclass-find egraph eclass-id)) + (pending egraph))))) + (setf (pending egraph) nil) + (loop + :for eclass-id :being :the :hash-key :of todo + :do (repair egraph eclass-id)))) + egraph) + + + +(defun root-eclasses (egraph) + "Find all eclasses that have no parents." + (loop + :for eclass-id :being + :the :hash-key :of (eclasses egraph) + :using (hash-value eclass) + :when (root-eclass-p eclass) + :collect eclass)) + +(defun enode< (a b) + (unless (eq a b) + (cond + ((eq a b) nil) + ;; if one is a vector, but not the other + ((and (not (vectorp a)) (vectorp b)) t) + ((and (vectorp a) (not (vectorp b))) nil) + ;; if a and b are symbols + ((and (symbolp a) (symbolp b)) (string< a b)) + ;; if one is a number, but not the other + ((and (not (numberp a)) (numberp b)) nil) + ((and (numberp a) (not (numberp b))) t) + ;; if they're both numbers + ((and (numberp a) (numberp b)) (< a b)) + ;; if they're both vectors: + ((and (vectorp a) (vectorp b)) + (loop + :for i :from 0 + :for el1 :across a + :for el2 :across b + :do (cond + ((equalp el1 el2) nil) + ((enode< el1 el2) (return t)) + (t (return nil))) + :finally + ;; if we get there it's because either the 2 vectors are equal, or + ;; one is the prefix of the other. + ;; + ;; So we return true, if a is shorter than b. + (< (length a) (length b))))))) + +(defun smallest-enodes (eclasses) + "Find the smallest enode from a set of eclasses." + (let* ((all-nodes (sort (apply #'concatenate 'vector + (mapcar #'enodes eclasses)) + #'enode<)) + (shortest (aref all-nodes 0))) + ;; It's possible to have many enodes with the same size, so we + ;; filter out all those that are bigger than the smallest. + (remove-if (lambda (enode) + (enode< shortest enode)) + all-nodes))) + + + +(defmethod add-form (egraph form) + "Add a FORM to an e-graph, creating e-classes if necessary. Returns an +eclass-id." + (egraph-add-enode egraph form)) + +(defmethod add-form (egraph (form cons)) + "Add a FORM to an e-graph, creating e-classes if necessary. Returns an +eclass-id." + (egraph-add-enode + egraph + ;; Convert FORM into an enode. + (apply #'vector + (car form) + (mapcar + (lambda (element) (add-form egraph element)) + (rest form))))) + +;; This one is used to add "forms" that were created by pattern +;; substitution (e.g. by a rewrite). The pattern matching codes works +;; with vectors. +(defmethod add-form (egraph (form vector)) + "Add a FORM to an e-graph, creating e-classes if necessary. Returns an +eclass-id." + (egraph-add-enode + egraph + ;; Convert FORM into an enode. + (apply #'vector + (aref form 0) + (loop :for i :from 1 :below (length form) + :collect (add-form egraph (aref form i)))) + #++ ;; TODO don't treat the first element in a special way + (map 'vector + (lambda (element) (add-form egraph element)) + form))) + +;; This is also required for adding forms created by pattern +;; substitution, because the matching and substitution is done on +;; enodes, it creates forms that contains instance of eclasses (the +;; bindings during matching binds to instances of elcasses). +(defmethod add-form (egraph (eclass eclass)) + "Add ECLASS to an e-graph, creating e-classes if necessary." + ;; assumes it's already in the egraph + (id eclass)) + +(defmethod add-input (egraph form) + (let ((eclass-id (add-form egraph form))) + (pushnew eclass-id (input-eclasses egraph)))) + + +;;; E-matching, rewrites, rules, etc. + +(defun match-enode (egraph enode pattern set-of-bindings) + ;; TODO support for variable-length matches + (etypecase enode + ((or symbol number) + (merge-sets-of-bindings + set-of-bindings + (list (match pattern enode)))) + (vector + (when (alexandria:length= enode pattern) + ;; This is getting very complicated because + ;; "match-eclass" returns a list of possible bindings... + (loop + :for eclass-id :across enode + :for subpattern :across pattern + :for new-set-of-bindings = + ;; TODO Optimization: check if match returns T + ;; Probably not worth it, as I may change that code altogether... + (merge-sets-of-bindings + set-of-bindings + ;; TODO This assumes that the first element of the enode is not an eclass + (list (breeze.pattern::match eclass-id subpattern))) + :then (merge-sets-of-bindings + new-set-of-bindings + (match-eclass egraph + (eclass egraph eclass-id) + subpattern new-set-of-bindings)) + :while new-set-of-bindings + :finally (return new-set-of-bindings)))))) + +(defun match-eclass (egraph eclass pattern &optional (set-of-bindings '(t))) + (check-type egraph egraph) + (check-type eclass eclass) + (etypecase pattern + (breeze.pattern:term + ;; The whole class "matches" + (list (breeze.pattern::make-binding pattern eclass))) + ((or vector symbol number) + ;; Find every enode that matches the pattern + (loop :for enode :across (enodes eclass) + :for new-set-of-bindings := (match-enode egraph enode pattern set-of-bindings) + :when new-set-of-bindings + :append new-set-of-bindings)))) + +(defun match-rewrite (egraph rewrite) + "Match 1 rewrite against an egraph, returns a list of substituted +forms." + (loop + :with (pattern . substitution) := rewrite + :for eclass-id :being :the :hash-key :of (eclasses egraph) + :using (hash-value eclass) + ;; :for eclass :in (eclasses egraph) + :for set-of-bindings = (match-eclass egraph eclass pattern) + :when set-of-bindings + :collect (list eclass + ;; Compute the substitutions + (mapcar (lambda (bindings) + (pattern-substitute substitution bindings)) + set-of-bindings)))) + +(defun apply-rewrite (egraph rewrite) + "Match REWRITE's pattern against EGRAPH. Add the new forms and merge +the corresponding ECLASSES. +Does NOT rebuild the egraph's invariants." + #++ (format t "~&Rewrite from ~s to ~s" + (rewrite-pattern rewrite) + (rewrite-template rewrite)) + (loop :for (eclass forms) :in (match-rewrite egraph rewrite) + :do (loop :for new-form :in forms + :for new-eclass-id = (add-form egraph new-form) + :for new-eclass = (eclass egraph new-eclass-id) + :do (maphash (lambda (parent-enode parent-eclass-id) + ;; (declare (ignore parent-enode)) + (add-parent new-eclass parent-enode parent-eclass-id)) + (parents eclass)) + :do (merge-eclass egraph (id eclass) new-eclass-id))) + egraph) + + +;;; Stream/Iterators + +(defgeneric stream-get (stream)) + +(defgeneric stream-next (stream)) + +(defgeneric stream-done-p (stream)) + +(defun collect (stream &key (limit)) + (if limit + (loop + repeat limit + until (stream-done-p stream) + collect (stream-get stream) + do (stream-next stream)) + (loop + until (stream-done-p stream) + collect (stream-get stream) + do (stream-next stream)))) + +(defun map-stream (fn stream &key (limit)) + (if limit + (loop + repeat limit + until (stream-done-p stream) + do (funcall fn (stream-get stream)) + do (stream-next stream)) + (loop + until (stream-done-p stream) + do (funcall fn (stream-get stream)) + do (stream-next stream)))) + + +(defun stream-sequence (seq) + "Create a stream out of a sequence, the stream will repeat the sequence +infinitely, but will be considered \"done\" as soon as the iterator is +past the last element of the sequence." + (let ((i 0) + (l (length seq))) + (if (zerop l) + (lambda (&optional method) + (ecase method + ((nil) nil) + (:next (progn (incf i) 0)) + (:done t))) + (lambda (&optional method) + (ecase method + ((nil) (elt seq (mod i l))) + (:next (progn (incf i) + (if (zerop l) + 0 + (zerop (mod i l))))) + (:done (<= l i))))))) + +(defun stream-constant (x) + "Create a stream out of one value." + (stream-sequence (vector x))) + +(defmethod stream-get ((stream function)) + (funcall stream)) + +(defmethod stream-next ((stream function)) + (funcall stream :next)) + +(defmethod stream-done-p ((stream function)) + (funcall stream :done)) + +;; TODO filter-stream + +(defun next-list-of-stream (streams) + "Given a sequence of streams, advance the sequence as a whole." + (etypecase streams + (cons (loop + for s in streams + for nextp = (stream-next s) + while nextp + finally (return nextp))) + (vector (loop + for s across streams + for nextp = (stream-next s) + while nextp + finally (return nextp))))) + + +(defun stream-product (streams) + "Create a stream that produces the Cartesian product of all the STREAMS." + (let ((donep nil)) + (lambda (&optional method) + (ecase method + (:next (setf donep (next-list-of-stream streams))) + (:done donep) + ((nil) (map 'list 'stream-get streams)))))) + +(defun stream-sequence-product (sequences) + "Create a stream that produces the Cartesian product of all the SEQUENCES." + (stream-product + (mapcar #'stream-sequence sequences))) + +#++ +(collect (stream-sequence-product '(#(a b) #(c d)))) +;; => ((A C) (B C) (A D) (B D)) + +(defun stream-concat (streams) + (if (zerop (length streams)) + (stream-sequence nil) + (let* ((s (stream-sequence streams)) + (current (stream-get s))) + (lambda (&optional method) + (ecase method + (:next (when (stream-next current) + (stream-next s) + (setf current (stream-get s)) + (stream-done-p current))) + (:done (stream-done-p current) (stream-done-p s)) + ((nil) (stream-get current))))))) + +#++ +(collect + (stream-concat (vector (stream-sequence '(a b)) (stream-constant 'c)))) +;; => (A B C) + + +;;; Streaming terms out of egraphs + +(defun stream-eclass (egraph eclass) + (stream-concat + (map 'vector + (lambda (enode) (stream-enode egraph enode)) + (enodes eclass)))) + +(defun stream-equivalent-eclasses (egraph eclass-id) + "Create a stream that will iterate over all the eclasses that are +equivalent to eclass-id." + (let* ((canonical-id (eclass-find egraph eclass-id)) + (id -1) + (union-find (union-find egraph)) + (l (length union-find))) + (flet ((find-next () + ;; Find the next equivalent id in the union-find data + ;; structure. + (loop + :for i = (incf id) + :while (< id l) + :for eclass-id = (aref union-find i) + :when (= canonical-id eclass-id) + :do (return id)))) + (find-next) + (lambda (&optional method) + (ecase method + (:next + (find-next)) + (:done (<= l id)) + ((nil) id)))))) + +(defun stream-enode (egraph enode) + (etypecase enode + ((or symbol number) (stream-constant enode)) + (vector (stream-product + (let ((i 0)) + (map 'vector + (lambda (eclass-id-or-constant) + (cond + ((plusp i) (stream-eclass egraph (eclass egraph eclass-id-or-constant))) + (t (incf i) (stream-constant eclass-id-or-constant)))) + enode)))))) + +(defun map-egraph (fn egraph &key limit) + (map-stream + fn + (stream-concat + (map 'vector (lambda (eclass) + (stream-eclass egraph eclass)) + ;; TODO %root-eclasses is the right one to call BUT some + ;; eclass'e parents are not set correctly during rewriting. + ;; (breeze.egraph::%root-eclasses egraph) + (root-eclasses egraph))) + :limit limit)) diff --git a/src/ensure-breeze.lisp b/src/ensure-breeze.lisp index cfc02042..a3a810f4 100644 --- a/src/ensure-breeze.lisp +++ b/src/ensure-breeze.lisp @@ -8,12 +8,28 @@ It is used, for example, by emacs in breeze.el. (cl:in-package #:cl-user) + +;; TODO "Checkpoints" +;; TODO Unload (e.g. delete-package) if it fails to load! +;; TODO _maybe_ add a variable *breeze-loaded-correctly-p* + (asdf:load-asd (merge-pathnames "../breeze.asd" *load-truename*)) +;; TODO error handling +;; +;; TODO if quicklisp is not available, check if all dependencies are +;; available before trying to load the whole system +;; +;; TODO _maybe_ fallback to vendored dependency systems if they can't +;; be found +;; +;; TODO some dependencies and subsystems could be make optional, maybe +;; this script could take care of setting up some *features*? + (unless (asdf:component-loaded-p "breeze") #+quicklisp (ql:quickload "breeze") #-quicklisp - (require '#:breeze) + (asdf:load-system '#:breeze) (format t "~&Breeze loaded!~%")) diff --git a/src/listener.lisp b/src/listener.lisp index ac8bd0a0..b7867f9e 100644 --- a/src/listener.lisp +++ b/src/listener.lisp @@ -2,7 +2,7 @@ (defpackage #:breeze.listener (:use :cl #:alexandria #:breeze.command) - (:documentation "Swank/Slynk wrapper") + (:documentation "RPC, REPL and more.") (:import-from #:breeze.xref #:classp #:function-designator-p) @@ -25,230 +25,6 @@ differences between swank and slynk." (*readtable* (cl:copy-readtable nil))) (eval (read-from-string string)))) - - -(defparameter *recent-forms* () - "A list of recently-evaluated forms (as strings).") - - - -#| - -TODO Would be nice to have a "Shadow-import all" restart. - -|# - - - -;; TODO Use a heap to get the N smallest values! -;; TODO Put that into utils? -(defmacro minimizing ((var - &key - (score-var (gensym "score")) - tracep) - &body body) - "Creates both a variable (let) and a function (flet) to keep track -of the instance of that had the smallest score." - (check-type var symbol) - `(let ((,var nil) - (,score-var)) - (flet ((,var (new-candidate new-score) - ,@(when tracep - `((format *debug-io* "~&new-candidate: ~s new-score: ~s" - new-candidate new-score))) - (when (and new-score - (or - ;; if it wasn't initialized already - (null ,var) - ;; it is initialized, but score is better - (< new-score ,score-var))) - (setf ,var new-candidate - ,score-var new-score)))) - ,@body - (values ,var ,score-var)))) - - -(defun find-most-similar-symbol (input) - (minimizing (candidate) - (do-symbols (sym) - (when (fboundp sym) - (candidate sym - (breeze.utils:optimal-string-alignment-distance* - input - (string-downcase sym) - 3)))))) - -;; (find-most-similar-symbol "prin") ;; => princ, 1 - -(defun find-most-similar-package (input) - (minimizing (candidate) - (loop :for package in (list-all-packages) - :for package-name = (package-name package) :do - (loop :for name in `(,package-name ,@(package-nicknames package)) :do - (candidate name - (breeze.utils:optimal-string-alignment-distance* - input - (string-downcase name) - 3)))))) - -#+ (or) -(progn - (find-most-similar-package "breeze.util") - ;; => breeze.utils, 1 - - (find-most-similar-package "commmon-lisp") - ;; => "COMMON-LISP", 1 - ) - -(defun find-most-similar-class (input) - (minimizing (candidate) - (do-symbols (sym) - (when (classp sym) - (candidate sym - (breeze.utils:optimal-string-alignment-distance* - input - (string-downcase sym) - 3)))))) - -(defvar *last-invoked-restart* nil - "For debugging purposes only") - -(defun resignal-with-suggestion-restart (input candidate condition) - ;; Ok, this is messy as hell, but it works - (unless - ;; We install a new restart - (with-simple-restart (use-suggestion - "Use \"~a\" instead of \"~a\"." - candidate input) - ;; with-simple-restart returns the _last evaluated_ form - t - ;; Then we signal the condition again - (error condition)) - ;; with-simple-restart will return nil and t if the restart was - ;; invoked - (let ((use-value (find-restart 'use-value condition))) - (setf *last-invoked-restart* (list candidate)) - (format *debug-io* "~&About to invoke the restart ~s with the value ~s." - use-value - candidate) - ;; (describe use-value) - ;; (inspect use-value) - (invoke-restart use-value candidate)))) - -(defun suggest (input candidate condition) - (message "Did you mean \"~a\"?" candidate) - (when candidate - (let ((restart (find-restart 'use-value condition))) - (or - (and restart (resignal-with-suggestion-restart - input candidate condition)) - (warn "Did you mean \"~a\"?~%~a" - candidate - (breeze.utils:indent-string - 2 - (breeze.utils:print-comparison - nil - (string-downcase candidate) - input))))))) - -(defgeneric condition-suggestion-input (condition) - (:documentation "Get input for \"find-most-similar-*\" functions from a condition") - ;; Default implementation - (:method (condition) - (cell-error-name condition)) - (:method ((condition undefined-function)) - (format *debug-io* "~&1") - (cell-error-name condition)) - (:method ((condition package-error)) - (let ((package-designator - (package-error-package condition))) - (if (stringp package-designator) - package-designator - #+sbcl ;; only tested on sbcl - (car - (slot-value condition - 'sb-kernel::format-arguments))))) - #+sbcl - (:method ((condition sb-ext:package-does-not-exist)) - (package-error-package condition)) - #+sbcl - (:method ((condition sb-pcl:class-not-found-error)) - (sb-kernel::cell-error-name condition))) - -;; (trace condition-suggestion-input) - -(defmacro defun-suggest (types) - `(progn - ,@(loop - :for type :in types - :collect - `(defun ,(symbolicate 'suggest- type) (condition) - (let* ((input (string-downcase (condition-suggestion-input condition))) - (candidate (,(symbolicate 'find-most-similar- type) input))) - #+ (or) - (format *debug-io* - ,(format nil - "~~&candidate ~(~a~): ~~s" - type) - candidate) - (if candidate - (suggest input candidate condition) - (error condition))))))) - -(defun-suggest - (symbol - package - class)) - -#+ (or) -(trace suggest-symbol - suggest-package - suggest-class) - -#+ (or) -(progn - ;; List the slot of a condition - (sb-kernel::condition-assigned-slots *condition*) - - ;; Get the first element of a condition's format arguments - (car - (slot-value *condition* - 'sb-kernel::format-arguments)) ) - -(defvar *last-condition* nil - "For debugging purposose only.") - -#+ (or) -(defparameter *condition* *last-condition* - "Just a quick way to save the last-condition.") - -#+ (or) -(type-of *condition*) -;; => SB-PCL::MISSING-SLOT - -(defun call-with-correction-suggestion (function) - "Funcall FUNCTION wrapped in a handler-bind form that suggest corrections." - (handler-bind - ((error #'(lambda (condition) - (setf *last-condition* condition) - (error condition)))) - (handler-bind - ;; The order is important!!! - ((undefined-function #'suggest-symbol) - #+sbcl (sb-ext:package-does-not-exist #'suggest-package) - #+sbcl (sb-int:simple-reader-package-error #'suggest-symbol) - #+ (or) - (package-error #'suggest-package) - #+sbcl - (sb-pcl:class-not-found-error #'suggest-class)) - (funcall function)))) - -;; (prin t) -;; (commmon-lisp:print :oups) -;; (cl:prin :oups) -;; (call-with-correction-suggestion (lambda () (eval '(prin)))) -;; (make-instance 'typos) - (defparameter *interactive-eval-hooks* '()) @@ -282,7 +58,6 @@ of the instance of that had the smallest score." (read-from-string string))))) (message (format-values-for-echo-area values)))) -;; TODO Use serapeum:run-hook (defun run-interactive-eval-after-hooks (string substring) (loop :for (name . hook) :in *interactive-eval-hooks* @@ -306,14 +81,19 @@ of the instance of that had the smallest score." (lambda () (prog1 (%interactive-eval substring) - (run-interactive-eval-after-hooks string substring)) - )))) + (run-interactive-eval-after-hooks string substring)))))) (defparameter *interactive-eval-last-context* () "For debugging only.") +(defparameter *last-parse* nil + "I sure love this trick!") + (define-command interactive-eval-command () "A command to interactively evaluate code." + (setf *last-parse* (breeze.lossless-reader:parse (buffer-string))) + (message "Parsed without signalling an error.") + #++ (let ((context (context*))) (setf *interactive-eval-last-context* context) (if (augment-context-by-parsing-the-buffer context) @@ -323,23 +103,10 @@ of the instance of that had the smallest score." (progn ;; TODO (pulse-momentary-highlight-region begin end) ;; TODO Find what's the value of *package* at this node... + #++ (let ((string (breeze.syntax-tree:node-raw node))) (interactive-eval string) ;; (message "~s" string) ))) ;; (message "Can't parse maybe?") ))) - - -;;; Evaluation history - -;; TODO cleanup *recent-forms* from time to time. -;; TODO maybe uses a hash-table instead of a list for *recent-forms* to keep a -;; kind of frequency-table - -(defun get-recent-interactively-evaluated-forms () - "Get the 50 most recently evaluated forms" - (loop :for form :in *recent-forms* - :for i :below 50 - :do (format t "~&~a~%" - (remove #\newline form)))) diff --git a/src/lossless-reader.lisp b/src/lossless-reader.lisp index bfc9105e..54a52592 100644 --- a/src/lossless-reader.lisp +++ b/src/lossless-reader.lisp @@ -1,35 +1,7 @@ -#| +(cl:in-package #:cl-user) -Ok, I'm tired of eclector... It is slow, it's brittle, it is hard to -make it do what I want (i.e. everything I do with eclector feels like -a huge kludge). - -I'm gonna try my hand at making a reader... Worst case scenario I end -up with more test case for my eclector-based parser. - -http://www.lispworks.com/documentation/HyperSpec/Body/02_.htm - -|# - -#| - -Error recovery - -- basic: use "synchronization points", places where it looks like a -good place to restart parsing after an invalid parse - -- it would be much easier to pin-point the source of the failure if we -start from a previous good state (incremental parsing) - -- I noticed that for lisp, a lot of things would be "easy" to parse -backward, this would help tremendously pin-pointing where a "bad -parse" begins. - -|# - - -(defpackage #:breeze.lossless-reader - (:documentation "A fast, lossless, robust and superficial reader for a superset of +(uiop:define-package #:breeze.lossless-reader + (:documentation "A fast, lossless, robust and superficial reader for a superset of common lisp.") (:use #:cl) (:import-from #:breeze.utils @@ -38,7 +10,162 @@ common lisp.") #:whitespacep) (:import-from #:alexandria #:when-let - #:when-let*)) + #:when-let*) + ;; Parsing state + (:export #:state + #:source + #:pos + #:tree + #:make-state + #:source-substring) + ;; Nodes + (:export #:+end+ + #:node + #:node-start + #:node-end + #:node-type + #:node-children + #:copy-node + #:valid-node-p + #:node-content) + ;; Node constructors + (:export #:block-comment + #:parens + #:punctuation + #:token + #:whitespace + #:line-comment + #:string + #:quote + #:quasiquote + #:dot + #:comma + #:sharp + #:sharp-char + #:sharp-function + #:sharp-vector + #:sharp-bitvector + #:sharp-uninterned + #:sharp-eval + #:sharp-binary + #:sharp-octal + #:sharp-hexa + #:sharp-complex + #:sharp-structure + #:sharp-pathname + #:sharp-feature + #:sharp-feature-not + #:sharp-radix + #:sharp-array + #:sharp-label + #:sharp-reference + #:sharp-unknown + #:current-package-symbol + #:keyword + #:uninterned-symbol + #:qualified-symbol + #:possibly-internal-symbol) + ;; Node copiers + (:export #:copy-block-comment + #:copy-parens + #:copy-sharpsign + #:copy-punctuation + #:copy-token + #:copy-whitespace + #:copy-line-comment + #:copy-string + #:copy-quote + #:copy-quasiquote + #:copy-dot + #:copy-comma + #:copy-sharp + #:copy-sharp-char + #:copy-sharp-function + #:copy-sharp-vector + #:copy-sharp-bitvector + #:copy-sharp-uninterned + #:copy-sharp-eval + #:copy-sharp-binary + #:copy-sharp-octal + #:copy-sharp-hexa + #:copy-sharp-complex + #:copy-sharp-structure + #:copy-sharp-pathname + #:copy-sharp-feature + #:copy-sharp-feature-not + #:copy-sharp-radix + #:copy-sharp-array + #:copy-sharp-label + #:copy-sharp-reference + #:copy-sharp-unknown) + ;; Node predicates + (:export #:block-comment-node-p + #:parens-node-p + #:punctuation-node-p + #:token-node-p + #:symbol-node-p + #:whitespace-node-p + #:line-comment-node-p + #:whitespace-or-comment-node-p + #:string-node-p + #:quote-node-p + #:quasiquote-node-p + #:dot-node-p + #:comma-node-p + #:sharp-node-p + #:sharp-char-node-p + #:sharp-function-node-p + #:sharp-vector-node-p + #:sharp-bitvector-node-p + #:sharp-uninterned-node-p + #:sharp-eval-node-p + #:sharp-binary-node-p + #:sharp-octal-node-p + #:sharp-hexa-node-p + #:sharp-complex-node-p + #:sharp-structure-node-p + #:sharp-pathname-node-p + #:sharp-feature-node-p + #:sharp-feature-not-node-p + #:sharp-radix-node-p + #:sharp-array-node-p + #:sharp-label-node-p + #:sharp-reference-node-p + #:sharp-unknown-node-p) + ;; State utilities + (:export #:at + #:at= + #:current-char + #:current-char= + #:next-char + #:next-char= + #:donep + #:valid-position-p + #:*state-control-string* + #:state-context + ;; parsing utilities + #:read-char* + #:find-all + #:not-terminatingp + #:read-string* + #:read-while + ;; sub parser + #:read-line-comment + #:read-parens + #:read-sharpsign-dispatching-reader-macro + #:read-punctuation + #:read-quoted-string + #:read-string + #:read-token + #:read-whitespaces + #:read-block-comment) + (:export + #:token-symbol-node) + (:export + ;; top-level parsing/unparsing + #:parse + #:unparse ;; maybe deprecate? + #:walk)) (in-package #:breeze.lossless-reader) @@ -67,11 +194,26 @@ common lisp.") ;; - current input base (base of numbers) ;; - current depth? ;; - is inside quasiquotation? + ;; - is inside quotes? + ;; - cache + ;; - labels and references (#n= and #n#) (:documentation "The reader's state")) (defun make-state (string) (make-instance 'state :source string)) +(defmethod print-object ((state state) stream) + (print-unreadable-object + (state stream :type t :identity nil) + (let ((excerpt (breeze.utils:around (source state) + (pos state)))) + (format stream "~s ~d/~d" + excerpt + (length excerpt) + (length (source state)))))) + + + (alexandria:define-constant +end+ -1) (defstruct (range @@ -86,7 +228,7 @@ common lisp.") :read-only t)) (defstruct (node - (:constructor node (type start end &optional children)) + ;; (:constructor node (type start end &optional children)) :constructor (:predicate nodep) (:include range)) @@ -96,46 +238,168 @@ common lisp.") (children '() :read-only t)) -(defun whitespace (start end) - (node 'whitespace start end)) - -(defun block-comment (start end) - (node 'block-comment start end)) +(defun node (type start end &optional children) + #++ (when (= +end+ end) + (break)) + (make-node + :type type + :start start + :end end + :children children)) -(defun line-comment (start end) - (node 'line-comment start end)) + +;;; Constructors + +(macrolet ((aux (type + &key + children + (name type) + no-constructor-p) + `(progn + ;; predicate + (defun + ,(alexandria:symbolicate type '-node-p) + (node) + ,(format nil "Is this a node of type ~s" type) + (and (nodep node) + (eq (node-type node) ',type) + node)) + ;; constructor + ,(unless no-constructor-p + `(defun ,name (start end + ,@(when children + (case children + (&optional (list '&optional 'children)) + ((t) (list 'children)) + (t (alexandria:ensure-list children))))) + (node ',type start end ,@(when children + (if (eq t children) + (list 'children) + (alexandria:ensure-list children)))))) + ;; copier + (defun ,(alexandria:symbolicate 'copy- name) + (node &key (start nil startp) + (end nil endp) + ,@(when children + (list `(children nil childrenp)))) + (node ',type + (if startp start (node-start node)) + (if endp end (node-end node)) + ,@(when children + `((if childrenp children (node-children node))))))))) + ;; TODO more of then needs children... + (aux whitespace) + (aux block-comment) + (aux line-comment) + (aux token) + (aux parens :children &optional :no-constructor-p t) + (aux punctuation :no-constructor-p t) + (aux string :name string-node) + (aux quote :name quote-node) + (aux quasiquote) + (aux dot) + (aux comma) + (aux sharp) + (aux sharp-char :children t) + (aux sharp-function :children t) + (aux sharp-vector) + (aux sharp-bitvector) + (aux sharp-uninterned) + (aux sharp-eval) + (aux sharp-binary) + (aux sharp-octal) + (aux sharp-hexa) + (aux sharp-complex) + (aux sharp-structure) + (aux sharp-pathname :children t) + (aux sharp-feature :children t) + (aux sharp-feature-not :children t) + (aux sharp-radix) + (aux sharp-array) + (aux sharp-label :children t) + (aux sharp-reference :children label) + (aux sharp-unknown)) (defun punctuation (type position) (node type position (1+ position))) -(defun token (start end) - (node 'token start end)) - (defun parens (start end &optional children) (node 'parens start end (if (nodep children) (list children) children))) -(macrolet ((p (type) - `(export - (defun - ,(alexandria:symbolicate type '-node-p) - (node) - ,(format nil "Is this a node of type ~s" type) - (and (nodep node) - (eq (node-type node) ',type)))))) - (p whitespace) - (p block-comment) - (p line-comment) - (p token) - (p parens)) +(defmethod print-object ((node node) stream) + (let ((*print-case* :downcase) + (children (node-children node))) + (format stream "(~:[node '~;~]~s ~d ~d~:[ ~s~;~@[ (list ~{~s~^ ~})~]~])" + (member (node-type node) + '(parens + token + whitespace + block-comment + line-comment + sharp-char + sharp-function + sharp-vector + sharp-bitvector + sharp-uninterned + sharp-eval + sharp-binary + sharp-octal + sharp-hexa + sharp-complex + sharp-structure + sharp-pathname + sharp-feature + sharp-feature-not + sharp-radix + sharp-array + sharp-label + sharp-reference + sharp-unknown)) + (node-type node) + (node-start node) + (node-end node) + (listp children) + children))) + +;; TODO make a test out of this +#++ +(mapcar + #'princ-to-string + (list + (node 'asdf 1 3) + (node 'asdf 1 3 (node 'qwer 3 5)) + (node 'asdf 1 3 (list (node 'qwer 3 5) + (node 'uiop 6 8))) + (parens 3 5))) + + +;;; Predicates (defun comment-node-p (node) "Is this node a block or line comment?" (or (line-comment-node-p node) (block-comment-node-p node))) +(defun whitespace-or-comment-node-p (node) + "Is this node a whitespace, a block comment or line comment?" + (or (whitespace-node-p node) + (comment-node-p node))) + +(defun symbol-node-p (node) + (and + (nodep node) + (member (node-type node) '(current-package-symbol + keyword + uninterned-symbol + qualified-symbol + possibly-internal-symbol)))) + + +;;; Content and range + (defun source-substring (state start end) "Get a (displaced) substring of the state's source string." (subseq-displaced (source state) @@ -189,32 +453,59 @@ common lisp.") ;; Could be further generalized by adding `&key key test`, and/or ;; making variants `at-if`, `at-if-not`. -(defun at (state position &optional char) +(defun at (state position) "Get the character at POSITION in the STATE's source. -Returns nil if POSITION is invalid. If the optional parameter CHAR is -not nil, further compare the char at POSITION with CHAR and return the -character if they're char=." +Returns nil if POSITION is invalid." (when (valid-position-p state position) - (when-let ((c (char (source state) position))) - (when (or (null char) (char= c char)) c)))) + (char (source state) position))) + +;; TODO add tests with case-sensitive-p = nil +;; TODO split into at= and at-equal +(defun at= (state position char &optional (case-insensitive-p t)) + "Compare the character at POSITION in the STATE's source with the parameter CHAR and returns the CHAR if they're char=. +Returns nil if POSITION is invalid." + (when-let ((c (at state position))) (and + (if case-insensitive-p + (char= c char) + (char-equal c char)) + c))) + +(defun current-char (state) + "Get the character at the current STATE's position, without changing +the position." + (at state (pos state))) -(defun current-char (state &optional char) +;; TODO add tests with case-sensitive-p = nil +;; TODO split into current-char= and current-char-equal +(defun current-char= (state char &optional (case-sensitive-p t)) "Get the character at the current STATE's position, without changing the position." - (at state (pos state) char)) + (at= state (pos state) char case-sensitive-p)) + +(defun next-char (state &optional (n 1)) + "Peek at the next character to be read, without changing the +position." + (at state (+ n (pos state)))) -(defun next-char (state &optional char) +;; TODO add tests with case-sensitive-p = nil +;; TODO split into next-char= and next-char-equal +(defun next-char= (state char &optional (n 1) (case-sensitive-p t)) "Peek at the next character to be read, without changing the position." - (at state (1+ (pos state)) char)) + (at= state (+ n (pos state)) char case-sensitive-p)) ;;; Low-level parsing helpers -(defun read-char* (state &optional char) +;; TODO add tests with case-sensitive-p = nil +;; TODO split into read-char and read-char= +;; TODO implement using current-char= +(defun read-char* (state &optional char (case-sensitive-p t)) (when-let ((c (current-char state))) (when (or (null char) - (char= c char)) + (if case-sensitive-p + (char= c char) + (char-equal c char))) (incf (pos state)) c))) @@ -232,19 +523,28 @@ the occurence of STRING." (list start end))))) ;; 2023-05-20 only used in read-token -(defun read-while (state predicate &aux (start (pos state))) +;; 2024-01-03 and read- dispatch reader macro +;; TODO return a range instead of a list +(defun read-while (state predicate &key (advance-position-p t) (start (pos state))) + "Returns nil or (list start end)" (loop - ;; :for guard :upto 10 + ;; :for guard :from 0 :for pos :from start :for c = (at state pos) + ;; :when (< 9000 guard) :do (error "read-while might be looping indefinitely...") :do (when (or (null c) (not (funcall predicate c))) (when (/= start pos) - (setf (pos state) pos) + (when advance-position-p (setf (pos state) pos)) (return (list start pos))) (return nil)))) ;; Will be useful for finding some synchronization points +;; ;; 2023-05-20 not used anymore, since I refactored read-block-comment +;; +;; TODO maybe add a callback instead of building up a list... (not +;; sure if it's worth it (performance-wise), it'll heavily depends on +;; how and _if_ I use this). (defun find-all (needle string) (when (and (plusp (length needle)) (plusp (length string))) @@ -256,7 +556,12 @@ the occurence of STRING." ;;; Actual reader -(defun read-whitespaces (state &aux (start (pos state))) +(defmacro defreader (name lambda-list &body body) + `(defun ,name (state ,@lambda-list &aux (start (pos state))) + (declare (ignorable start)) + ,@body)) + +(defreader read-whitespaces () (loop :for pos :from start :for c = (at state pos) @@ -265,7 +570,7 @@ the occurence of STRING." (setf (pos state) pos) (return (whitespace start pos))))) -(defun read-block-comment (state &aux (start (pos state))) +(defreader read-block-comment () "Read #||#" (when (read-string* state "#|" nil) (loop @@ -298,23 +603,210 @@ the occurence of STRING." (t (setf situation 'other)))))))) -(defun read-line-comment (state) +(defreader read-line-comment () "Read ;" - (let ((start (pos state))) - (when (read-char* state #\;) - (let ((newline (search #.(format nil "~%") - (source state) - :start2 (pos state)))) - (if newline - (progn - (setf (pos state) (1+ newline)) - (line-comment start (pos state))) - ;; TODO (defun (setf donep) ...) - (progn - (setf (pos state) (length (source state))) - (line-comment start +end+))))))) - -(defun read-punctuation (state) + (when (read-char* state #\;) + (let ((newline (search #.(format nil "~%") + (source state) + :start2 (pos state)))) + (setf (pos state) (if newline + newline + (length (source state)))) + (line-comment start (pos state))))) + +;; TODO rename read-integer +(defun read-number (state &optional (radix 10)) + (let ((range (read-while state #'(lambda (char) (digit-char-p char radix))))) + (when range + (let ((*read-base* radix)) + (values (read-from-string (apply #'source-substring state range)) range))))) + +;;; TODO in the following read-sharpsign-* functions, number should be +;;; renamed "prefix" + +(defun read-sharpsign-backslash (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\\) + (decf (pos state)) + (let ((token (when (valid-position-p state (1+ (pos state))) + (read-token state)))) + (node 'sharp-char start (if token (pos state) +end+) token)))) + +(defun read-any* (state) + (multiple-value-bind (whitespaces form) + (read-any state t) + (let ((children (remove-if #'null (list whitespaces form))) + (end (if (and form + (valid-node-p form)) + (pos state) + +end+))) + (values end children)))) + +(defun %read-sharpsign-any (state start type) + (multiple-value-bind (end children) + (read-any* state) + (node type start end children))) + +(defun read-sharpsign-quote (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\') + (%read-sharpsign-any state start 'sharp-function))) + +(defun read-sharpsign-left-parens (state start number) + (declare (ignore number)) + ;; N.B. we use current-char instead of read-char, because we don't + ;; want to consume the left-parens right away. + (when (current-char= state #\() + (let ((form (read-parens state))) + (node 'sharp-vector start (if form (pos state) +end+) form)))) + +(defun read-sharpsign-asterisk (state start length) + (declare (ignore length)) + (when (read-char* state #\*) + (multiple-value-bind (bits range) + (read-number state 2) + (declare (ignore range)) + ;; TODO check (- (cdr range) (car range)) <= length + (node 'sharp-bitvector start (pos state) bits)))) + +(defun read-sharpsign-colon (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\:) + (let* ((token-start (pos state)) + (token (read-token state)) + (end (pos state))) + (node 'sharp-uninterned start end + (or token + (token token-start end)))))) + +(defun read-sharpsign-dot (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\.) + (%read-sharpsign-any state start 'sharp-eval))) + +(defun %read-sharpsign-number (state start type radix) + (let ((n (read-number state radix))) + (node type start (if n (pos state) +end+)))) + +(defun read-sharpsign-b (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\b nil) + (%read-sharpsign-number state start 'sharp-binary 2))) + +(defun read-sharpsign-o (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\b nil) + (%read-sharpsign-number state start 'sharp-octal 8))) + +(defun read-sharpsign-x (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\x nil) + (%read-sharpsign-number state start 'sharp-hexa 16))) + +(defun read-sharpsign-r (state start radix) + (when (read-char* state #\r nil) + (let ((n (read-number state radix))) + (node 'sharp-radix start (if n (pos state) +end+))))) + +(defun read-sharpsign-c (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\c nil) + (let ((form (read-parens state))) + (node 'sharp-complex start (if form (pos state) +end+) form)))) + +(defun read-sharpsign-a (state start length) + (declare (ignore length)) + (when (read-char* state #\a nil) + (let ((form (read-parens state))) + (node 'sharp-array start (if form (pos state) +end+) form)))) + +(defun read-sharpsign-s (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\s nil) + (%read-sharpsign-any state start 'sharp-structure))) + +(defun read-sharpsign-p (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\p nil) + (%read-sharpsign-any state start 'sharp-pathname))) + +(defun read-sharpsign-equal (state start number) + (when (read-char* state #\=) + (multiple-value-bind (end children) + (read-any* state) + ;; TODO sharp-label would benefit from having it own data + ;; structure, this is abusing the children + (node 'sharp-label start end + (append + (when (and (integerp number) + (<= 0 number)) + (list :label number)) + (when children + (list :form children))))))) + +(defun read-sharpsign-sharpsign (state start number) + (when (read-char* state #\#) + (node 'sharp-reference start (if (and (integerp number) + (<= 0 number)) + (pos state) + +end+) + number))) + +(defun read-sharpsign-plus (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\+) + (%read-sharpsign-any state start 'sharp-feature))) + +(defun read-sharpsign-minus (state start number) + (declare (ignore number)) + ;; TODO (if number) => invalid syntax + (when (read-char* state #\-) + (%read-sharpsign-any state start 'sharp-feature-not))) + +;; TODO #) and # are **invalid** +;; See https://www.lispworks.com/documentation/HyperSpec/Body/02_dh.htm +(defreader read-sharpsign-dispatching-reader-macro () + "Read reader macros #..." + (when (read-char* state #\#) + (let ((number (read-number state))) + (or + (some + (lambda (fn) + (funcall fn state start number)) + '(read-sharpsign-backslash + read-sharpsign-quote + read-sharpsign-left-parens + read-sharpsign-asterisk + read-sharpsign-colon + read-sharpsign-dot + read-sharpsign-b + read-sharpsign-o + read-sharpsign-x + read-sharpsign-r + read-sharpsign-c + read-sharpsign-a + read-sharpsign-s + read-sharpsign-p + read-sharpsign-equal + read-sharpsign-sharpsign + read-sharpsign-plus + read-sharpsign-minus)) + ;; Invalid syntax OR custom reader macro + (sharp-unknown start +end+))))) + + +(defreader read-punctuation () "Read ' or `" (when-let* ((current-char (current-char state)) (foundp (member current-char @@ -326,7 +818,7 @@ the occurence of STRING." (#\# . sharp) (#\page . page)) :key #'car))) - (prog1 (punctuation (cdar foundp) (pos state)) + (prog1 (punctuation (cdar foundp) start) (incf (pos state))))) (defun read-quoted-string (state delimiter escape &optional validp) @@ -334,7 +826,7 @@ the occurence of STRING." is ESCAPE. Optionally check if the characters is valid if VALIDP is provided." (let ((start (pos state))) - (when (at state (pos state) delimiter) + (when (at= state (pos state) delimiter) (loop ;; :for guard :upto 10 :for pos :from (1+ (pos state)) @@ -354,7 +846,7 @@ provided." (setf (pos state) pos) (return (list start 'invalid)))))))) -(defun read-string (state) +(defreader read-string () "Read \"\"" (when-let ((string (read-quoted-string state #\" #\\))) (apply #'node 'string string))) @@ -368,35 +860,115 @@ http://www.lispworks.com/documentation/HyperSpec/Body/02_ad.htm" +whitespaces+) :test #'char=)))) -(defun not-terminatingp-nor-pipe (c) - "Test whether a character is terminating or #\|. See +(defun not-terminatingp-nor-escape (c) + "Test whether a character is terminating or #\| or #\\. See http://www.lispworks.com/documentation/HyperSpec/Body/02_ad.htm" (and c (not (position c '#. (concatenate 'string ";\"'(),`" - "|" + "|\\" +whitespaces+) :test #'char=)))) -(defun read-token (state &aux (start (pos state))) +(defreader read-backslash () + (when (read-char* state #\\) + (when (read-char* state) + (list start (pos state))))) + +(defreader read-pipe () + (when (current-char= state #\|) + (read-quoted-string state #\| #\\))) + +#++ +(progn + (trace + not-terminatingp + not-terminatingp-nor-pipe + read-token + read-backslash + read-pipe + read-while + donep) + (untrace)) + +(defun %token-symbol-node (string &optional (start 0) (end (length string))) + (when (and string start end + (< -1 start end) + (plusp (length string))) + (case (count #\: string :start start :end end) + (0 + ;; "x" + (node 'current-package-symbol start end)) + (1 + (or + ;; ":x" + (when (char= #\: (char string start)) + (node 'keyword (1+ start) end)) + ;; "#:x" + (when (and (< 2 (- end start)) + (char= #\# (char string start)) + (char= #\: (char string (1+ start)))) + (node 'uninterned-symbol (+ 2 start) end)) + ;; p:x + (let ((position (position #\: string :start start :end end))) + (and (not (= position (1- end))) + (node 'qualified-symbol + start end + (list + (node 'package-name start position) + (node 'symbol-name (1+ position) end))))))) + ;; p::x + (2 (let* ((first (position #\: string :start start :end end))) + (and + (/= start first) + (< (1+ first) (1- end)) + (char= #\: (char string (1+ first))) + (node 'possibly-internal-symbol + start end + (list + (node 'package-name start first) + (node 'symbol-name (+ 2 first) end))))))))) + +(defun token-symbol-node (state token-node) + "Extract information about the package-name and symbol-name of a token, if it can. +Returns a new node with one of these types: + + - current-package-symbol + - keyword + - uninterned-symbol + - qualified-symbol + - possibly-internal-symbol" + (%token-symbol-node (source state) + (node-start token-node) + (node-end token-node))) + +(defreader read-token () "Read one token." (loop + :with escape-once :for char = (current-char state) :while (not-terminatingp char) - :for part = (if (char= char #\|) - (read-quoted-string state #\| #\\) - (read-while state #'not-terminatingp-nor-pipe)) + :for part = (or + (read-backslash state) + (read-pipe state) + (read-while state #'not-terminatingp-nor-escape)) :while (and part (not-terminatingp (current-char state)) (not (donep state))) :finally (return (unless (= start (pos state)) - (token start (if part (second part) +end+)))))) + (let ((end (if part (second part) +end+))) + (token start end) + ;; for debugging + #++ + (node 'token start end + ;; Only for debugging + (source-substring state start end))))))) ;; TODO Do something with this, to help error recovery, or at least ;; tell the user something. -(defun read-extraneous-closing-parens (state &aux (start (pos state))) +(defreader read-extraneous-closing-parens () (when (read-char* state #\)) (make-node :type :extraneous-closing-parens :start start @@ -409,7 +981,7 @@ http://www.lispworks.com/documentation/HyperSpec/Body/02_ad.htm" (defparameter *state-control-string* - "~{position: ~D char: ~s context: «~a»~}") + "position: ~D char: ~s context: «~a»") (defun state-context (state) (let* ((pos (pos state)) @@ -418,40 +990,43 @@ http://www.lispworks.com/documentation/HyperSpec/Body/02_ad.htm" ,(at state pos) ,(breeze.utils:around string pos)))) + ;; don't forget to handle dotted lists -(defun read-parens (state &aux (start (pos state))) +(defreader read-parens () (when (read-char* state #\() ;; Read while read-any != nil && char != ) (loop ;; :for guard :below 1000 ; infinite loop guard :while (not (read-char* state #\))) ; good ending :for el = (read-any state) ; mutual recursion - :if (valid-node-p el) + :when el :collect el :into content - :else - :do (return (if (donep state) - (parens start +end+ content) - (error "This is a bug: read-any returned an invalid node, but we're not done reading the file...~%~?" - *state-control-string* - (state-context state)))) + :unless (valid-node-p el) + :do (return (parens start +end+ content)) :finally (return (parens start (pos state) content))))) -(defun read-any (state) - (or - (some #'(lambda (fn) - (funcall fn state)) - '(read-whitespaces - read-block-comment - read-punctuation - read-string - read-line-comment - read-token - read-parens ; recursion - read-extraneous-closing-parens)) - (unless (donep state) - (error "This is a bug: read-any read nothing and would return nil, but we're not done reading the file...~%~?" - *state-control-string* - (state-context state))))) +;; TODO add tests with skip-whitespaces-p set +(defun read-any (state &optional skip-whitespaces-p) + (if skip-whitespaces-p + (let ((whitespaces (read-whitespaces state)) + (form (read-any state nil))) + (values whitespaces form)) + (or + (some #'(lambda (fn) + (funcall fn state)) + '(read-whitespaces + read-block-comment + read-sharpsign-dispatching-reader-macro + read-string + read-line-comment + read-punctuation + read-token + read-parens ; recursion + read-extraneous-closing-parens)) + (unless (donep state) + (error "This is a bug: read-any read nothing and would return nil, but we're not done reading the file...~%~?" + *state-control-string* + (state-context state)))))) ;;; Putting it all toghether @@ -460,8 +1035,10 @@ http://www.lispworks.com/documentation/HyperSpec/Body/02_ad.htm" "Parse a string, stop at the end, or when there's a parse error." (setf (tree state) (loop + ;; :for i :from 0 :for node-start = (pos state) :for node = (read-any state) + ;; :when (< 9000 i) :do (error "Really? over 9000 top-level forms!? That must be a bug...") :when node :collect node :while (and (valid-node-p node) @@ -519,39 +1096,40 @@ http://www.lispworks.com/documentation/HyperSpec/Body/02_ad.htm" (return result)))) -;;; TODO Unparse -;;; -;;; N.B. an "unparse" that doesn't change anything is useless except -;;; for testing. because we could just use the source string, -;;; unchanged. -;;; -;;; How to make changes to the nodes, without actually modifying them? -;;; -;;; I thought about keeping track of "before" "after" and "instead" changes. -;;; keyed by the node itself... -;;; +;;; Unparse ;; Should I pass the depth here too? (defun write-node (node state stream) - (write-string (source state) stream - :start (start node) - :end (if (no-end-p node) - (length (source state)) - (end node)))) - -;; (trace write-node) -;; (untrace) - -(defun %unparse (tree state stream depth) - (if (listp tree) - (mapcar (lambda (node) (%unparse node state stream (1+ depth))) tree) - (write-node tree state stream))) - -(defun unparse (state &optional (stream t)) + (when node + (write-string (source state) stream + :start (start node) + :end (if (no-end-p node) + (length (source state)) + (end node))))) + +(defun %unparse (tree state stream depth transform) + (when tree + (if (listp tree) + (mapcar (lambda (node) + (%unparse (funcall transform node) + state stream (1+ depth) + transform)) + tree) + (case (node-type tree) + (parens + (write-char #\( stream) + (%unparse (node-children tree) state stream depth transform) + (unless (no-end-p tree) + (write-char #\) stream))) + (t + (write-node (funcall transform tree) state stream)))))) + +(defun unparse (state &optional (stream t) (transform #'identity)) (if stream (%unparse (tree state) state (if (eq t stream) *standard-output* stream) - 0) + 0 + transform) ;; if stream is nil (with-output-to-string (out) - (%unparse (tree state) state out 0)))) + (%unparse (tree state) state out 0 transform)))) diff --git a/src/pattern.lisp b/src/pattern.lisp index f4b6a4bc..958cce14 100644 --- a/src/pattern.lisp +++ b/src/pattern.lisp @@ -1,8 +1,25 @@ ;;;; Trying to design a DSL for small refactors (defpackage #:breeze.pattern - (:documentation "TODO Pattern matching stuff") - (:use #:cl)) + (:documentation "Pattern matching") + (:use #:cl) + (:export #:compile-pattern) + (:export #:defpattern + #:match + #:ref + #:term + #:maybe + #:*match-skip*) + (:export #:iterate + #:iterator-done-p + #:iterator-value) + ;; Working with match results + (:export #:merge-sets-of-bindings + #:find-binding + #:pattern-substitute) + (:export #:make-rewrite + #:rewrite-pattern + #:rewrite-template)) (in-package #:breeze.pattern) @@ -27,7 +44,7 @@ (eq (ref-name a) (ref-name b)))) -;; Decision: I chose "term" and not "variable" to avoid clashed with +;; Decision: I chose "term" and not "variable" to avoid clashes with ;; cl:variable (defstruct (term (:constructor term (name)) @@ -35,6 +52,11 @@ (:predicate termp)) (name nil :type symbol :read-only t)) +(defmethod print-object ((term term) stream) + (print-unreadable-object + (term stream :type t :identity t) + (format stream "~s" (term-name term)))) + (defun term= (a b) (and (termp a) (termp b) @@ -63,29 +85,36 @@ ;; TODO Maybe generalize "maybe" and "zero-or-more" into "repetition" -(defstruct (maybe - (:constructor maybe (pattern)) - :constructor - (:predicate maybep)) - (pattern nil :read-only t)) - -(defun maybe= (a b) - (and (maybep a) - (maybep b) - (pattern= (maybe-pattern a) - (maybe-pattern b)))) - -(defstruct (zero-or-more - (:constructor zero-or-more (pattern)) +(defstruct (repetition + (:constructor repetition (pattern min max &optional name)) :constructor - :predicate) - (pattern nil :read-only t)) + (:predicate repetitionp) + (:include term)) + (pattern nil :read-only t) + (min nil :read-only t) + (max nil :read-only t)) + +(defun repetition= (a b) + (and (repetitionp a) + (repetitionp b) + (pattern= (repetition-pattern a) (repetition-pattern b)) + (= (repetition-min a) (repetition-min b)) + (let ((ma (repetition-max a)) + (mb (repetition-max a))) + (or (eq ma mb) (and (numberp ma) (numberp mb)) (= ma mb))) + (or (null (repetition-name a)) + (null (repetition-name b)) + (eq (repetition-name a) + (repetition-name b))))) + +(defun maybe (pattern &optional name) + (repetition pattern 0 1 name)) + +(defun zero-or-more (pattern &optional name) + (repetition pattern 0 nil name)) -(defun zero-or-more= (a b) - (and (zero-or-more-p a) - (zero-or-more-p b) - (pattern= (zero-or-more-pattern a) - (zero-or-more-pattern b)))) + +;;; WIP Alternations (defstruct (alternation (:constructor alternation (pattern)) @@ -99,6 +128,8 @@ (pattern= (alternation-pattern a) (alternation-pattern b)))) + + (defmethod pattern= (a b) (equal a b)) @@ -118,8 +149,7 @@ (def ref) (def term) (def typed-term) - (def maybe) - (def zero-or-more) + (def repetition) (def alternation)) @@ -132,24 +162,39 @@ (defun term-symbol-p (x) (symbol-starts-with x #\?)) +(defparameter *term-pool* nil + "A pool of terms, used to share terms across patterns created by +independent calls to compile-pattern.") + +(defun compile-pattern (pattern) + "Compiles a PATTERN (specified as a list). Returns 2 values: the +compiled pattern and *term-pool*. If *term-pool* is nil when +compile-pattern is called, a new one is created." + (if *term-pool* + (values (%compile-pattern pattern) *term-pool*) + (let ((*term-pool* (make-hash-table))) + (compile-pattern pattern)))) + ;; Default: leave as-is -(defmethod compile-pattern (pattern) pattern) +(defmethod %compile-pattern (pattern) pattern) ;; Compile symbols -(defmethod compile-pattern ((pattern symbol)) +(defmethod %compile-pattern ((pattern symbol)) (cond - ((term-symbol-p pattern) (term pattern)) + ((term-symbol-p pattern) + (or (gethash pattern *term-pool*) + (setf (gethash pattern *term-pool*) (term pattern)))) (t pattern))) ;; Compile lists -(defmethod compile-pattern ((pattern cons)) +(defmethod %compile-pattern ((pattern cons)) ;; Dispatch to another method that is eql-specialized on the firt ;; element of the list. (compile-compound-pattern (first pattern) pattern)) ;; Default list compilation: recurse and convert to vector. (defmethod compile-compound-pattern (token pattern) - (map 'vector #'compile-pattern pattern)) + (map 'vector #'%compile-pattern pattern)) ;; Compile (:the ...) (defmethod compile-compound-pattern ((token (eql :the)) pattern) @@ -162,26 +207,21 @@ ;; TODO Check length of "rest" (ref (second pattern))) -;; Helper function for compound patterns that can take an arbitrary -;; number of subpatterns. -(defun rest-or-second (list) - (if (cddr list) (rest list) (second list))) -;; (rest-or-second '(a b c)) => '(b c) -;; (rest-or-second '(a b)) => 'b - ;; Compile (:maybe ...) (defmethod compile-compound-pattern ((token (eql :maybe)) pattern) - (maybe (compile-pattern (rest-or-second pattern)))) + ;; TODO check the length of "pattern" + (maybe (%compile-pattern (second pattern)) (third pattern))) ;; Compile (:zero-or-more ...) (defmethod compile-compound-pattern ((token (eql :zero-or-more)) pattern) - (zero-or-more (compile-pattern (rest-or-second pattern)))) + (zero-or-more (%compile-pattern (rest pattern)))) ;; Compile (:alternation ...) (defmethod compile-compound-pattern ((token (eql :alternation)) patterns) - (alternation (compile-pattern (rest-or-second patterns)))) + (alternation (%compile-pattern (rest patterns)))) +;;; Re-usable, named patterns (defmacro defpattern (name &body body) `(setf (gethash ',name *patterns*) @@ -192,59 +232,109 @@ (defun ref-pattern (pattern) (check-type pattern ref) + ;; TODO rename terms???? (or (gethash (ref-name pattern) *patterns*) (error "Failed to find the pattern ~S." (ref-name pattern)))) +;;; Iterator: +;;; - takes care of "recursing" into referenced patterns +;;; - conditionally skips inputs +;;; - works on vectors only, for my sanity +;;; - I want to make it possible to iterate backward, hence the "step" + ;; Will I regret implemeting this? (defstruct iterator + ;; The vector being iterated on vector + ;; The current position in the vector (position 0) + ;; How much to advance the position per iteration (step 1) + ;; The iterator to return when the current one is done parent) +#++ +(defun iterator-depth (iterator) + (if (null (iterator-parent iterator)) + 0 + (1+ (iterator-depth (iterator-parent iterator))))) + (defun iterator-done-p (iterator) "Check if there's any values left to iterator over." (check-type iterator iterator) + ;; Simply check if "position" is out of bound. (not (< -1 (iterator-position iterator) (length (iterator-vector iterator))))) (defun iterator-push (iterator vector) + "Create a new iterator on VECTOR, with ITERATOR as parent. Returns the +new iterator." (check-type iterator iterator) (check-type vector vector) (make-iterator :vector vector :parent iterator)) (defun iterator-maybe-push (iterator) + "If ITERATOR is not done and the current value is a reference, \"push\" +a new iterator." (if (iterator-done-p iterator) iterator (let ((value (iterator-value iterator))) (if (refp value) - (iterator-push iterator (ref-pattern value)) + (iterator-maybe-push (iterator-push iterator (ref-pattern value))) iterator)))) (defun iterator-maybe-pop (iterator) + "If ITERATOR is done and has a parent, return the next parent." (check-type iterator iterator) (if (and (iterator-done-p iterator) (iterator-parent iterator)) - (iterator-maybe-pop (iterator-parent iterator)) + (let ((parent (iterator-parent iterator))) + ;; Advance the position + (incf (iterator-position parent) + (iterator-step parent)) + ;; return the parent + (iterator-maybe-pop parent)) iterator)) -(defun iterate (vector) +(defun iterate (vector &key (step 1)) "Create a new iterator." (check-type vector vector) - (iterator-maybe-push (make-iterator :vector vector))) - - -(defun iterator-next (iterator) - "Advance the iterator. Might return a whole new iterator." + (let ((iterator + (iterator-maybe-push + (make-iterator :vector vector :step step)))) + (if (iterator-skip-p iterator) + (iterator-next iterator) + iterator))) + +(defvar *match-skip* nil + "Controls wheter to skip a value when iterating.") + +(defun iterator-skip-p (iterator &optional (match-skip *match-skip*)) + (when (and match-skip (not (iterator-done-p iterator))) + (funcall match-skip (iterator-value iterator)))) + +(defun %iterator-next (iterator) + "Advance the iterator exactly once. Might return a whole new iterator." (check-type iterator iterator) + ;; Advance the position (incf (iterator-position iterator) (iterator-step iterator)) (iterator-maybe-push (iterator-maybe-pop iterator))) +(defun iterator-next (iterator) + "Advance the iterator, conditionally skipping some values. Might return +a whole new iterator." + (check-type iterator iterator) + (loop :for new-iterator = (%iterator-next iterator) + :then (%iterator-next new-iterator) + :while (iterator-skip-p new-iterator) + :finally (return new-iterator))) + (defun iterator-value (iterator) + "Get the value at the current ITERATOR's position." (check-type iterator iterator) (when (iterator-done-p iterator) (error "No more values in this iterator.")) @@ -252,99 +342,279 @@ (iterator-position iterator))) +;;; Bindings (e.g. the result of a successful match) + +(defun make-empty-bindings () t) + +(defun make-binding (term input) + (list (cons term input))) + +(defun merge-bindings (bindings1 bindings2) + (flet ((name (x) + (if (termp x) (term-name x) x))) + (cond + ((eq t bindings1) bindings2) + ((eq t bindings2) bindings1) + ((or (eq nil bindings1) (eq nil bindings2)) nil) + (t + ;; TODO It would be possible to pass the bindings into all "match" + ;; functions and methods. It would allow to detect conflicting + ;; bindings earlier and stop the matching process earlier. + ;; + ;; N.B. a disjoint-set data structure could help detect cycles in + ;; the bindings. + ;; + ;; TODO use a hash-table ffs + (delete-duplicates + (sort (append bindings1 bindings2) + (lambda (a b) + (let ((na (name (car a))) + (nb (name (car b)))) + (if (string= na nb) + (unless (eql (cdr a) (cdr b)) + (return-from merge-bindings nil)) + ;; (error "Conflicting bindings: ~a ~a" a b) + (string< na nb))))) + :key (alexandria:compose #'name #'car) + :test #'string=))))) + +(defun merge-sets-of-bindings (set-of-bindings1 set-of-bindings2) + "Merge two set of bindings (list of list of bindings), returns a new +set of bindings. +Matching a pattern against a set of values (e.g. an egraph) will yield +a set of independant bindings. During the macthing process, we might +need to refine the \"current\" set of bindings. Long-story short, this +is analoguous to computing the Cartesian product of the two sets of +bindings and keeping only those that have not conflicting bindings." + (loop :for bindings1 :in set-of-bindings1 + :append (loop :for bindings2 :in set-of-bindings2 + :for merged-bindings = (breeze.pattern::merge-bindings + bindings1 bindings2) + :when merged-bindings + :collect merged-bindings))) + +(defun find-binding (bindings term-or-term-name) + (when bindings + (if (termp term-or-term-name) + (assoc term-or-term-name bindings) + (assoc term-or-term-name bindings + :key #'term-name)))) + +;;; Matching atoms + +;; Basic "equal" matching (defmethod match (pattern input) (equal pattern input)) +;; Match a term (create a binding) (defmethod match ((pattern term) input) - (cons pattern input)) + (make-binding pattern input)) +;; Match a typed term (creates a binding) (defmethod match ((pattern typed-term) input) (when (typep input (typed-term-type pattern)) - (cons pattern input))) + (make-binding pattern input))) -#++ +;; Recurse into a referenced pattern (defmethod match ((pattern ref) input) (match (ref-pattern pattern) input)) +;; Match a string literal (defmethod match ((pattern string) (input string)) (string= pattern input)) +;; "nil" must match "nil" (defmethod match ((pattern null) (input null)) t) -(defmethod match ((pattern null) input) +;; "nil" must not match any other symbols +(defmethod match ((pattern null) (input symbol)) nil) + +;;; Matching sequences + +(defmethod match ((pattern iterator) (input iterator)) + (loop + :with bindings = (make-empty-bindings) + ;; Iterate over the pattern + :for pattern-iterator := pattern + :then (iterator-next pattern-iterator) + ;; Iterate over the input + :for input-iterator := input + :then (iterator-next input-iterator) + :until (or (iterator-done-p pattern-iterator) + (iterator-done-p input-iterator)) + :for new-bindings = (match + (iterator-value pattern-iterator) + (iterator-value input-iterator)) + :if new-bindings + ;; collect all the bindings + :do + ;; (break) + (setf bindings (merge-bindings bindings new-bindings)) + ;; The new bindings conflicted with the existing ones... + (unless bindings (return nil)) + :else + ;; failed to match, bail out of the whole function + :do (return nil) + :finally + ;; We advance the input iterator to see if there are still + ;; values left that would not be skipped. + (when (and (not (iterator-done-p input-iterator)) + (iterator-skip-p input-iterator)) + (setf input-iterator (iterator-next input-iterator))) + (return + ;; We want to match the whole pattern, but wheter we + ;; want to match the whole input is up to the caller. + (when (iterator-done-p pattern-iterator) + (values (or bindings t) + (if (iterator-done-p input-iterator) + nil + input-iterator)))))) + +(defmethod match ((pattern term) (input iterator)) + (multiple-value-bind (bindings input-remaining-p) + (match (iterate (vector pattern)) input) + (unless input-remaining-p + bindings))) + +(defmethod match ((pattern vector) (input vector)) + (multiple-value-bind (bindings input-remaining-p) + (match (iterate pattern) (iterate input)) + (unless input-remaining-p + bindings))) + + +;;; Matching alternations + +(defmethod match ((pattern alternation) input) + (some (lambda (pat) (match pat input)) + (alternation-pattern pattern))) + + +;;; Matching repetitions + #++ -(defmethod match ((pattern sequence) input) - (error "Only vector patterns are supported.")) +(defmethod match ((pattern maybe) input) + (or (alexandria:when-let ((bindings (match (maybe-pattern pattern) input))) + (if (maybe-name pattern) + (merge-bindings bindings (make-binding pattern input)) + bindings)) + (not input))) + +#++ +(defmethod match ((pattern zero-or-more) (input null)) + t) + +(defmethod match ((pattern repetition) (input vector)) + (loop + :with bindings = (make-empty-bindings) + :with pat = (repetition-pattern pattern) + :with input-iterator := (iterate input) + :for i :from 0 + :do (multiple-value-bind (new-bindings new-input-iterator) + (match (iterate pat) input-iterator) + ;; (break) + (if new-bindings + ;; collect all the bindings (setf bindings + ;; (merge-bindings bindings new-bindings)) + (progn + (setf bindings (merge-bindings bindings new-bindings)) + ;; TODO check if bindings is nil after merging. + ) + ;; No match + (if (<= (repetition-min pattern) i) + (return bindings) + (return nil))) + (if new-input-iterator + (setf input-iterator new-input-iterator) + ;; No more input left + (if (<= (repetition-min pattern) i) + (return bindings) + (return nil)))))) + +;; TODO +;; (defmethod match ((pattern repetition) (input iterator))) + + +;;; Convenience automatic coercions + +(defmethod match ((pattern vector) (input iterator)) + (match (iterate pattern) input)) (defmethod match ((pattern vector) (input sequence)) (match pattern (coerce input 'vector))) -(defmethod match ((pattern iterator) (input iterator)) - (match (iterator-value pattern) (iterator-value input))) +(defmethod match ((pattern repetition) (input sequence)) + (match pattern (coerce input 'vector))) -;; (trace iterator-next iterator-value iterator-push iterator-maybe-pop) -(defmethod match ((pattern vector) (input vector)) - (or (loop - ;; Iterate over the pattern - :for pattern-iterator := (iterate pattern) :then (iterator-next pattern-iterator) - :until (iterator-done-p pattern-iterator) - ;; :for pat = (iterator-value pattern-iterator) - ;; Iterate over the input - :for input-iterator := (iterate input) :then (iterator-next input-iterator) - :until (iterator-done-p input-iterator) - ;; :for in = (iterator-value input-iterator) - ;; recurse - :for match = #++ (match pat in) - (match pattern-iterator input-iterator) - ;; debug print - ;; :do (format *debug-io* "~%pat: ~s in: ~s" pat in) - :unless match - ;; failed to match, bail out of the whole function - :do (return-from match nil) - :when (listp match) - ;; collect all the bindings - ;; TODO We might want to "merge" the bindings. - :append match) - t)) + +;;; Match substitution + +(defun pattern-substitute (pattern bindings &optional (result-type 'vector)) + (when pattern + ;; Patterns are never compiled to lists + (check-type pattern atom) + (flet ((substitute1 (x) + (etypecase x + (term + (alexandria:if-let ((binding (find-binding bindings x))) + (cdr binding) + ;; TODO this could signal a condition (binding not + ;; found) + x)) + ((or symbol number) x)))) + (if (vectorp pattern) + (map result-type + ;; Note: we could've use map to recurse directly into + ;; pattern-subtitute, but not doing so make tracing + ;; (and debugging) tremenduously easier. + #'(lambda (subpattern) + (if (vectorp subpattern) + (pattern-substitute subpattern bindings result-type) + (substitute1 subpattern))) + pattern) + (substitute1 pattern))))) -#++ -(defmethod skip-input-p (x) - (and (symbolp x) - (char= #\< (char (symbol-name x) 0)))) + +;;; Rules and rewrites -#++ -(defmethod match ((pattern vector) (input vector) - &aux (i 0) (j 0)) - (labels - ((pattern () (aref pattern i)) - (input () (aref input j)) - (advance-pattern () (incf i)) - (advance-input () - (loop - :do (incf j) - :while (and (< j (length input)) - (skip-input-p (input)))))) - (loop - :for guard :below 1000 - :for (match . bindings) = (multiple-value-list - (match (pattern) (input))) - :unless match - :return nil - :append bindings - :do (advance-pattern) - :while (< i (length pattern)) - :do (advance-input) - :while (< j (length input))))) -#++ -(let ((pattern - (list-vector `(defun ?name ?ordinary-lambda-list ?body))) - (input - (list-vector `(defun foo (x y) (+ x y))))) - (match pattern input)) +;; TODO "rules" would be "bidirectional" and "rewrites" wouldn't. +;; TODO (defun rule (a b) ...) +;; TODO (defun make-rewrite (antecedant consequent) ...) + +#++ (progn + (defclass abstract-rule () ()) + + (defclass rule (abstract-rule) ()) + + (defun make-rule (a b) + (let ((*term-pool* (make-hash-table))) + (list :rule + (compile-pattern a) + (compile-pattern b)))) + + (defun make-rewrite (a b) + (let ((*term-pool* (make-hash-table))) + (list :rewrite + (compile-pattern a) + (compile-pattern b))))) + +(defun make-rewrite (pattern template) + (let ((*term-pool* (make-hash-table))) + (cons + (compile-pattern pattern) + (compile-pattern template)))) + +(defun rewrite-pattern (rewrite) + "Get the pattern of a REWRITE rule." + (car rewrite)) + +(defun rewrite-template (rewrite) + "Get the template of a REWRITE rule." + (cdr rewrite)) diff --git a/src/refactor.lisp b/src/refactor.lisp index 6c590a94..c809a1cd 100644 --- a/src/refactor.lisp +++ b/src/refactor.lisp @@ -143,8 +143,7 @@ defun." (read-string-then-insert "Name: " "~a (") (read-string-then-insert ;; Who needs to loop...? - "Enter the arguments: " "~a)~%)") - (backward-char)) + "Enter the arguments: " "~a)~%)")) (define-command insert-defun () "Insert a defun form." @@ -198,13 +197,14 @@ defun." (let ((package-name (read-string "Name of the package: " - (infer-package-name-from-file (context-buffer-file-name*))))) + (infer-package-name-from-file (buffer-file-name))))) (when *insert-defpackage/cl-user-prefix* (insert "(cl:in-package #:cl-user)~%~%~")) (if nil ; TODO (insert "(uiop:define-package ") (insert "(defpackage ")) + ;; TODO don't insert the (in-package ...) if it already exists (insert "#:~a~ ~% (:documentation \"\")~ @@ -384,7 +384,7 @@ defun." found." (let* ((previous-in-package-form (find-nearest-sibling-in-package-form nodes (or outer-node - (context-point*))))) + (point))))) (when previous-in-package-form (let* ((package-designator (in-package-node-package previous-in-package-form)) @@ -455,7 +455,7 @@ For debugging purposes ONLY.") (defun suggest-system-definition () "When in an .asd file" - (when (ends-with-subseq ".asd" (context-buffer-name*) + (when (ends-with-subseq ".asd" (buffer-name) :test #'string-equal) 'insert-asdf)) @@ -557,9 +557,9 @@ a message and stop the current command." (defun maybe-ask-to-load-system () - (if-let ((file-name (context-buffer-file-name*))) + (if-let ((file-name (buffer-file-name))) (multiple-value-bind (status system) - (breeze.asdf:loadedp (context-buffer-file-name*)) + (breeze.asdf:loadedp (buffer-file-name)) (when (eq :not-loaded status) (when (ask-y-or-n-p "The current file is part of the system \"~a\", but has not been loaded yet. Do you want to load it now? (y/n) " (asdf:component-name system)) @@ -571,7 +571,7 @@ a message and stop the current command." (define-command quickfix () "Given the context, suggest some applicable commands." - (maybe-ask-to-load-system) + (ignore-errors (maybe-ask-to-load-system)) (augment-context-by-parsing-the-buffer (context*)) (check-in-package) (let* (;; Compute the applicable commands @@ -592,27 +592,3 @@ a message and stop the current command." #+nil (quickfix :buffer-string " " :point 3) - - - -(defun command-to-emacs-lisp (command &optional stream) - "Take the symbol COMMAND generates the emacs lisp code to create an -emacs command," - (let ((docstring (command-docstring command))) - (format stream - "(defun breeze-~(~a~) ()~ - ~% ~s~ - ~% (interactive)~ - ~% (breeze-run-command ~(\"~a\"~)))" - command - docstring - (symbol-package-qualified-name command)))) - -#++ -(alexandria:with-output-to-file (output - (breeze.utils:breeze-relative-pathname "src/breeze-commands.el") - :if-exists :supersede) - (loop :for command :in (all-commands) - :do (command-to-emacs-lisp command output) - (terpri output) - (terpri output))) diff --git a/scratch-files/report.lisp b/src/report.lisp similarity index 68% rename from scratch-files/report.lisp rename to src/report.lisp index a1d22975..99475a5f 100644 --- a/scratch-files/report.lisp +++ b/src/report.lisp @@ -1,74 +1,76 @@ -(defpackage #:breeze.report - (:documentation "Using breeze's code to generate report to improve breeze.") - (:use #:cl) - (:import-from #:breeze.lossless-reader - #:parse - #:tree - #:node-content - #:node-start - #:node-end - #:node-type - #:source-substring - #:comment-node-p - #:whitespace-node-p - #:line-comment-node-p)) - -(in-package #:breeze.report) +#| -#++ ;; this is annoying af... -(setf (cdr (assoc 'slynk:*string-elision-length* slynk:*slynk-pprint-bindings*)) nil) +FIXME I originally named this "report" because I wanted +something "holistic", but now I started calling this "listing", which +is not holistic. +TODO I _could_ generate objects instead of directly generating +html... that way it _could_ be possible to generate something else +than html. -(defun find-all-breeze-systems () - "Find all systems defined in breeze.asd." - (let ((result ()) - (asd-pathname (asdf:system-source-file 'breeze))) - (asdf:map-systems (lambda (system) - ;; TODO Perhaps use asdf:primary-system-name - (when (equal asd-pathname - (asdf:system-source-file system)) - (push system result)))) - result)) +TODO Nice to haves: line numbers -#++ (mapcar 'asdf:coerce-name (find-all-breeze-systems)) +|# -(defun enough-breeze (pathname) - "Given a pathname, return the relative pathname from the root of the -breeze project (system)." - (uiop:enough-pathname - pathname - (asdf:system-source-directory 'breeze))) +(defpackage #:breeze.report + (:documentation "Using breeze's code to generate report to improve breeze.") + (:use #:cl #:breeze.lossless-reader)) -(defun remove-leading-semicolons (string) - "Remove leading semicolons (e.g. from line comments)." - (cl-ppcre:regex-replace-all - (cl-ppcre:create-scanner "^[ ;]+" - :multi-line-mode t) - string "")) +(in-package #:breeze.report) #++ -(remove-leading-semicolons "; ; ; ") -;; => "" - +(ql:quickload "cl-ppcre") +#++ ;; this is annoying af... +(setf (cdr (assoc 'slynk:*string-elision-length* slynk:*slynk-pprint-bindings*)) nil) (defun paragraphs (string) + "Split a string in \"paragraphs\" (a bit like markdown does, where two +newlines or more marks the start of a new paragraph)." + ;; TODO What if it starts with newlines? or if it's only newlines; I + ;; should probably use string-trim before split. (cl-ppcre:split (cl-ppcre:create-scanner "\\n\\n+" :multi-line-mode t :single-line-mode t) string)) -#+example +#++;; TODO Make a test (paragraphs (format nil "asd~5%qwe~%ert~2%jkl")) + + +(defun remove-leading-semicolons (string) + "Remove leading semicolons (e.g. from line comments)." + (cl-ppcre:regex-replace-all + (cl-ppcre:create-scanner "^[ ;]+" + :multi-line-mode t) + string "")) + +#++ +(remove-leading-semicolons "; ; ; ") +;; => "" +(defun escape-html (string) + (cl-ppcre:regex-replace-all + ;; I'm sure this is rock solid /s + (cl-ppcre:create-scanner "<((?!a |\/a|br).*?)>" + :multi-line-mode t) + string + "<\\1>")) +#++ +(progn + (escape-html "
") + (escape-html "") + (escape-html "") + (escape-html "=> (# #)")) + (defun page-node-p (node) "Is the node (from the lossless parser) a new-page (^L) character?" @@ -82,7 +84,7 @@ breeze project (system)." :with page = nil :for node :in (tree state) - :when (page-node-p node) + :when (and page (page-node-p node)) :do (push (nreverse page) pages) (setf page nil) :do (push node page) @@ -91,18 +93,28 @@ breeze project (system)." (push (nreverse page) pages)) (return (nreverse pages)))) -;; TODO rename to parse-system-file, maybe (probably)? + + +(defun enough-breeze (pathname) + "Given a pathname, return the relative pathname from the root of the +breeze project (system)." + (uiop:enough-pathname + pathname + (asdf:system-source-directory 'breeze))) + + (defun parse-system (&optional (system 'breeze)) "Parse (with lossless-reader) all files we want to include." - ;; TODO include files from other systems in this project. ;; TODO include all files that are tracked under git... (loop - :for file :in (breeze.asdf:system-files - system - :include-asd (asdf:primary-system-p system)) + :for file :in (sort (breeze.asdf:find-all-related-files system) + #'string< + :key #'namestring) :for filename = (enough-breeze file) :for content-str = (alexandria:read-file-into-string file) - :for state = (parse content-str) + :for state = (progn + (format *trace-output* "~&Parsing file ~s..." file) + (parse content-str)) :collect (list filename state (pages state)))) #++ @@ -112,8 +124,8 @@ breeze project (system)." ;; TODO move to utils; add tests... (defun nrun (list predicate) - "If the first element of LIST satisfies PREDICATE, destructively -extract the first run of elements that satisfies PREDICATE. Returns + "Destructively extract the first run of elements that satisfies +PREDICATE, if the first element of LIST satisfies PREDICATE, . Returns the run and update LIST's first cons to point to the last element of the run." (when (and list @@ -156,6 +168,7 @@ the run." list)) ;; => (1 3), (3 4 5) + (defun line-comment-or-ws (node) (and node @@ -171,9 +184,6 @@ the run." (let ((node-list (tree (parse (format nil "; c~% (+ 2 2) #| |#"))))) (group-line-comments node-list)) -;; (defun render-line-comments (nodes)) - - (defun page-title-node (page) "Try to infer the page's title. (Reminder: page is a list of node)" (loop @@ -186,7 +196,63 @@ the run." (defun render-line-comment (out comment) (format out "~{

~a

~%~}" - (paragraphs comment))) + (paragraphs (escape-html comment)))) + +#++ +(defun render-node (out state node) + (format out "~a" + (escape-html + (node-content state node)))) + + +;; This assumes the packages are loaded in the current image! +(defun cl-token-p (string) + (multiple-value-bind + (value error) + (ignore-errors (read-from-string string)) + (and (not (typep error 'error)) + (eq #.(find-package "CL") + (symbol-package value))))) + +(defun token-style (state node) + (if (valid-node-p node) + (let ((content (node-content state node))) + (cond + ((char= #\: (char content 0)) 'keyword) + ((numberp (ignore-errors (read-from-string content))) 'number) + ((alexandria:starts-with-subseq "check-" content) 'special) + ((position #\: content) 'symbol) + ((cl-token-p content) 'symbol))) + 'syntaxerror)) + +(defun render-escaped (out string) + (write-string (escape-html string) out)) + +(defun escaped-node-content (state node) + (escape-html (node-content state node))) + +(defun render-node (out state node &optional (depth 0)) + (case (node-type node) + (string + (format out "~a" + (escaped-node-content state node))) + (token + (alexandria:if-let ((style (token-style state node))) + (format out "~a" + (token-style state node) + (node-content state node)) + (render-escaped out (node-content state node)))) + (parens + (format out "(" + (valid-node-p node) + (min (1+ depth) 6)) + (map nil (lambda (node) + (render-node out state node (1+ depth))) + (node-children node)) + (format out ")")) + (t (format out "~a" + (string-downcase (node-type node)) + (escaped-node-content state node))))) (defun render-page (out state page) "Render 1 page as html, where PAGE is a list of nodes." @@ -203,10 +269,11 @@ the run." (render-line-comment out (remove-leading-semicolons (source-substring state start end))))) (;; don't print whitespace nodes - (whitespace-node-p node)) + (or (whitespace-node-p node) (page-node-p node))) (t - ;; TODO I should escape STRING - (format out "~%
~a
" (node-content state node)))))) + (format out "~%
")
+          (render-node out state node)
+          (format out "~%
"))))) (defmacro with-html-file ((stream-var filename) &body body) `(alexandria:with-output-to-file (,stream-var @@ -214,6 +281,7 @@ the run." :if-exists :supersede) (labels ((fmt (&rest rest) (apply #'format out rest))) + (fmt "") (fmt "") ;; https://github.com/emareg/classlesscss (fmt "") @@ -259,28 +327,32 @@ the run." (format nil "docs/listing-~a.html" (cl-ppcre:regex-replace-all "/" (asdf:coerce-name system) "--"))) + + (defun render (system &aux (pathname (system-listing-pathname system))) - (with-html-file (system out pathname) - ;; TODO "back to listings" + (format *debug-io* "~&Rendering listing for system ~s" system) + (with-system-listing (system out pathname) + ;; Table of content (fmt "
    ") (loop :for (filename state pages) :in files :do (fmt "
  1. ") (fmt "~a" (link-to-file filename)) - (when (breeze.utils:length>1? pages) + (progn ;;when (breeze.utils:length>1? pages) (fmt "
      ") (loop :for page :in pages :for i :from 1 :for page-title = (let ((node (page-title-node page))) (when node - (breeze.utils:summarize + (escape-html (remove-leading-semicolons (node-content state node))))) :do (fmt "
    1. ~a
    2. " (link-to-page filename i page-title))) (fmt "
    ")) (fmt "
  2. ")) (fmt "
") + ;; The actual content (loop :for (filename state pages) :in files :for number-of-pages = (length pages) @@ -290,41 +362,11 @@ the run." :for page :in pages :for i :from 1 :do - (when (> number-of-pages 1) - (fmt "

Page ~d

" (page-id filename i) i)) + (if (> number-of-pages 1) + (fmt "
" (page-id filename i)) + (fmt "
" (page-id filename i))) (render-page out state page)))) pathname) #++ (render 'breeze) - -#++ -(mapcar 'render (find-all-breeze-systems)) - -(defun listings.html () - (with-html-file (out (breeze.utils:breeze-relative-pathname "docs/listings.html")) - (fmt "
    ") - (loop - :for system :in (find-all-breeze-systems) - :for name = (asdf:coerce-name system) - :for file = (file-namestring (system-listing-pathname system)) - :do (fmt "
  1. ~a
  2. " file name)) - (fmt "
"))) - -#| - -FIXME I originally named this "report" because I wanted -something "holistic", but now I started calling this "listing", which -is not holistic. - -TODO In the same vein... I would like to have _all_ the listings in -the same file (currently 1 file per system). I want this because it -would be easier to convert to something else afterwards. - -TODO I _could_ generate objects instead of directly generating -html... that way it _could_ be possible to generate something else -than html. - -TODO Nice to haves: line numbers - -|# diff --git a/src/string-utils.lisp b/src/string-utils.lisp new file mode 100644 index 00000000..f7d6b978 --- /dev/null +++ b/src/string-utils.lisp @@ -0,0 +1,267 @@ +;;;; String manipulation utilities + +(in-package #:breeze.utils) + +(deftype string-designator () '(or string character symbol)) + +(defun optimal-string-alignment-distance (vec-a vec-b) + "Compute an edit distance between two vector." + (let* ((m (length vec-a)) + (n (length vec-b)) + (diff-0 (make-array (list (1+ n)) :element-type 'integer)) + (diff-1 (make-array (list (1+ n)) :element-type 'integer)) + (diff-2 (make-array (list (1+ n)) :element-type 'integer))) + + (loop :for i :upto n :do + (setf (aref diff-1 i) i)) + (setf (aref diff-0 0) 1) + + (flet ((a (index) (aref vec-a (1- index))) + (b (index) (aref vec-b (1- index))) + (diff-0 (index) (aref diff-0 index)) + (diff-1 (index) (aref diff-1 index)) + (diff-2 (index) (aref diff-2 index))) + (loop :for i :from 1 :upto m :do + (loop :for j :from 1 :upto n + :for cost = (if (eq (a i) (b j)) 0 1) ;; aka substitution-cost + :do + (setf (aref diff-0 j) (min + (1+ (diff-1 j)) ;; deletion + (1+ (diff-0 (1- j))) ;; insertion + (+ cost (diff-1 (1- j))) ;; substitution + )) + ;; transposition + (when (and (< 1 i) (< 1 j) + (eq (a i) (b (1- j))) + (eq (a (1- i)) (b j))) + (setf (aref diff-0 j) (min (diff-0 j) + (+ cost (diff-2 (- j 2))))))) + (when (/= m i) + (let ((tmp diff-2)) + (setf diff-2 diff-1 + diff-1 diff-0 + diff-0 tmp + (aref diff-0 0) (1+ i))))) + (diff-0 n)))) + +(defun optimal-string-alignment-distance* (vec-a vec-b max-distance) + "Compute an edit distance between two vector. Stops as soon as +max-distance is reached, returns nil in that case." + (unless (> (abs (- (length vec-a) + (length vec-b))) + max-distance) + (let* ((m (length vec-a)) + (n (length vec-b)) + (diff-0 (make-array (list (1+ n)) :element-type 'integer)) + (diff-1 (make-array (list (1+ n)) :element-type 'integer)) + (diff-2 (make-array (list (1+ n)) :element-type 'integer))) + + (loop :for i :upto n :do + (setf (aref diff-1 i) i)) + (setf (aref diff-0 0) 1) + + (flet ((a (index) (aref vec-a (1- index))) + (b (index) (aref vec-b (1- index))) + (diff-0 (index) (aref diff-0 index)) + (diff-1 (index) (aref diff-1 index)) + (diff-2 (index) (aref diff-2 index))) + (loop + :for min-distance = nil + :for i :from 1 :upto m :do + (loop :for j :from 1 :upto n + ;; aka substitution-cost + :for cost = (if (eq (a i) (b j)) 0 1) + :do + (setf (aref diff-0 j) (min + ;; deletion + (1+ (diff-1 j)) + ;; insertion + (1+ (diff-0 (1- j))) + ;; substitution + (+ cost (diff-1 (1- j))))) + ;; transposition + (when (and (< 1 i) (< 1 j) + (eq (a i) (b (1- j))) + (eq (a (1- i)) (b j))) + (setf (aref diff-0 j) (min (diff-0 j) + (+ cost (diff-2 (- j 2)))))) + (when (or (null min-distance) + (> min-distance (diff-0 j))) + ;; (format *debug-io* "~&new min-distance ~s" min-distance) + (setf min-distance (diff-0 j)))) + ;; (format *debug-io* "~&~s ~s" i diff-0) + (when (and (> i 1) + (>= min-distance max-distance)) + #+ (or) + (format *debug-io* "~&min-distance ~s > max-distance ~s" + min-distance max-distance) + (return-from optimal-string-alignment-distance*)) + (when (/= m i) + (let ((tmp diff-2)) + (setf diff-2 diff-1 + diff-1 diff-0 + diff-0 tmp + (aref diff-0 0) (1+ i))))) + (diff-0 n))))) + + + +(defun repeat-string (n string &optional stream) + (if stream + (loop :repeat n :do (write-string string stream)) + (with-output-to-string (output) + (repeat-string n string output)))) + + + +(defun split-by-newline (string) + (uiop:split-string string :separator '(#\Newline))) + +#++ +(split-by-newline "a +b +c") + + + +(defun indent-string (indentation string) + "Prepend INDENTATION spaces at the beginning of each line in STRING." + (check-type indentation (integer 0)) + (with-input-from-string (input string) + (with-output-to-string (output) + (loop :for line = (read-line input nil nil) + :while line + :do + (repeat-string indentation " " output) + (write-string line output) + (terpri output))))) + +#| +(indent-string 4 (format nil "a~%b")) +" a +b +" +|# + +(defun leading-whitespaces (string) + (with-input-from-string (input string) + ;; Skip the first line + (when (read-line input nil nil) + (loop :for line = (read-line input nil nil) + :while line + :for leading-whitespaces = (position-if-not #'whitespacep line) + :when leading-whitespaces + :minimize leading-whitespaces)))) + +(defun remove-indentation (string) + (let ((indentation (leading-whitespaces string))) + (with-input-from-string (input string) + (with-output-to-string (output) + (loop :for line = (read-line input nil nil) + :while line + :for leading-whitespaces = (position-if-not #'whitespacep line) + :if (and leading-whitespaces + (>= leading-whitespaces indentation)) + :do (write-string (subseq-displaced line indentation) output) + :else + :do (write-string line output) + :do (write-char #\newline output)))))) + +;; TODO IIRC this function sucked, I think it might just need some +;; *print- variables set to the right thing... TO TEST +(defun print-comparison (stream string1 string2) + "Print two (close) string in a way that the difference are easier to see." + (let* ((mismatch (mismatch string1 string2))) + (format stream "~&~a~%~a|~%~a" + string1 + (if (null mismatch) + "" + (repeat-string mismatch "=")) + string2))) + +#| +(print-comparison nil "abc" "adc") + +(print-comparison nil "abce" "abcd") + +(print-comparison nil +(string-downcase 'system-files) +(string-downcase 'sytsem-files)) +"system-files +==| +sytsem-files" +|# + + +;; all this to get rid of cl-ppcre xD +(defun remove-parentheses (string) + "Return new string with the parts between parentheses removed, along +with the spaces following the closing parentheses. Do not support +nested parentheses." + (with-output-to-string (o) + (loop + :with skipping + :for c across string + :do (cond + ;; Opening paren + ((and (not skipping) + (char= #\( c)) + (setf skipping c)) + ;; Closing paren + ((and skipping + (char= #\) c)) + (setf skipping c)) + ;; Common case + ((not skipping) (write-char c o)) + ;; Non-space char after closing paren + ((and skipping + (char= #\) skipping) + (char/= #\Space c)) + (setf skipping nil) + (write-char c o)))))) + +(defun summarize (string) + "Keep only the first sentence, remove parenthesis." + (remove-parentheses + (alexandria:if-let (position (position #\. string)) + (subseq string 0 position) + string))) + + +;; This is a good candidate for a funtion where the unit tests would +;; provide great examples for the documentation. +(defun around (string position &optional (around 10)) + "Returns part of STRING, from POSITIONITION - AROUND to POSITIONITION + +AROUND. Add elipseses before and after if necessary." + (let* ((min-size (1+ (* 2 around))) + (before (- position around)) + (start (max 0 before)) + (after (+ start min-size)) + (end (min (length string) after)) + (start (max 0 (min start (- end min-size)))) + (ellipsis-left (max 0 (min 3 start))) + (ellipsis-right (max 0 (min 3 (- (length string) end))))) + (with-output-to-string (out) + (loop :for i :below ellipsis-left :do (write-char #\. out)) + (write-string string out :start start :end end) + (loop :for i :below ellipsis-right :do (write-char #\. out))))) + + +(alexandria:define-constant +whitespaces+ + #. (coerce '(#\Space #\Newline #\Backspace #\Tab #\Linefeed #\Page #\Return + #\Rubout) + 'string) + :test 'equal) + +(defun whitespacep (char) + "Is CHAR a whitespace?" + (position char +whitespaces+ :test #'char=)) + +(defun trim-whitespace (string) + (string-trim +whitespaces+ string)) + +(defun symbol-package-qualified-name (symbol) + "Given a SYMBOL return a string of the form package:symbol." + (let ((*print-escape* t) + (*package* (find-package "KEYWORD"))) + (prin1-to-string symbol))) diff --git a/src/suggestion.lisp b/src/suggestion.lisp new file mode 100644 index 00000000..885ebe94 --- /dev/null +++ b/src/suggestion.lisp @@ -0,0 +1,183 @@ + +(in-package #:breeze.listener) + + +;; TODO Use a heap to get the N smallest values! +;; TODO Put that into utils? +(defmacro minimizing ((var + &key + (score-var (gensym "score")) + tracep) + &body body) + "Creates both a variable (let) and a function (flet) to keep track +of the instance of that had the smallest score." + (check-type var symbol) + `(let ((,var nil) + (,score-var)) + (flet ((,var (new-candidate new-score) + ,@(when tracep + `((format *debug-io* "~&new-candidate: ~s new-score: ~s" + new-candidate new-score))) + (when (and new-score + (or + ;; if it wasn't initialized already + (null ,var) + ;; it is initialized, but score is better + (< new-score ,score-var))) + (setf ,var new-candidate + ,score-var new-score)))) + ,@body + (values ,var ,score-var)))) + + +(defun find-most-similar-symbol (input) + (minimizing (candidate) + ;; TODO do-symbols only iterate on *package* + (do-symbols (sym) + (when (fboundp sym) + (candidate sym + (breeze.utils:optimal-string-alignment-distance* + input + (string-downcase sym) + 3)))))) + +;; (find-most-similar-symbol "prin") ;; => princ, 1 + +(defun find-most-similar-package (input) + (minimizing (candidate) + (loop :for package in (list-all-packages) + :for package-name = (package-name package) :do + (loop :for name in `(,package-name ,@(package-nicknames package)) :do + (candidate name + (breeze.utils:optimal-string-alignment-distance* + input + (string-downcase name) + 3)))))) + +#+ (or) +(progn + (find-most-similar-package "breeze.util") + ;; => breeze.utils, 1 + + (find-most-similar-package "commmon-lisp") + ;; => "COMMON-LISP", 1 + ) + +(defun find-most-similar-class (input) + (minimizing (candidate) + (do-symbols (sym) + (when (classp sym) + (candidate sym + (breeze.utils:optimal-string-alignment-distance* + input + (string-downcase sym) + 3)))))) + +(defvar *last-invoked-restart* nil + "For debugging purposes only") + +(defun resignal-with-suggestion-restart (input candidate condition) + ;; Ok, this is messy as hell, but it works + (unless + ;; We install a new restart + (with-simple-restart (use-suggestion + "Use \"~a\" instead of \"~a\"." + candidate input) + ;; with-simple-restart returns the _last evaluated_ form + t + ;; Then we signal the condition again + (error condition)) + ;; with-simple-restart will return nil and t if the restart was + ;; invoked + (let ((use-value (find-restart 'use-value condition))) + (setf *last-invoked-restart* (list candidate)) + (format *debug-io* "~&About to invoke the restart ~s with the value ~s." + use-value + candidate) + (invoke-restart use-value candidate)))) + +(defun suggest (input candidate condition) + (message "Did you mean \"~a\"?" candidate) + (when candidate + (let ((restart (find-restart 'use-value condition))) + (or + (and restart (resignal-with-suggestion-restart + input candidate condition)) + (warn "Did you mean \"~a\"?~%~a" + candidate + (breeze.utils:indent-string + 2 + (breeze.utils:print-comparison + nil + (string-downcase candidate) + input))))))) + +(defgeneric condition-suggestion-input (condition) + (:documentation "Get input for \"find-most-similar-*\" functions from a condition") + ;; Default implementation + (:method (condition) + (cell-error-name condition)) + (:method ((condition undefined-function)) + (format *debug-io* "~&1") + (cell-error-name condition)) + (:method ((condition package-error)) + (let ((package-designator + (package-error-package condition))) + (if (stringp package-designator) + package-designator + #+sbcl ;; only tested on sbcl + (car + (slot-value condition + 'sb-kernel::format-arguments))))) + #+sbcl + (:method ((condition sb-ext:package-does-not-exist)) + (package-error-package condition)) + #+sbcl + (:method ((condition sb-pcl:class-not-found-error)) + (sb-kernel::cell-error-name condition))) + +;; (trace condition-suggestion-input) + +(defmacro defun-suggest (types) + `(progn + ,@(loop + :for type :in types + :collect + `(defun ,(symbolicate 'suggest- type) (condition) + (let* ((input (string-downcase (condition-suggestion-input condition))) + (candidate (,(symbolicate 'find-most-similar- type) input))) + #+ (or) + (format *debug-io* + ,(format nil + "~~&candidate ~(~a~): ~~s" + type) + candidate) + (if candidate + (suggest input candidate condition) + (error condition))))))) + +(defun-suggest + (symbol + package + class)) + +(defvar *last-condition* nil + "For debugging purposose only.") + + +(defun call-with-correction-suggestion (function) + "Funcall FUNCTION wrapped in a handler-bind form that suggest corrections." + (handler-bind + ((error #'(lambda (condition) + (setf *last-condition* condition) + (error condition)))) + (handler-bind + ;; The order is important!!! + ((undefined-function #'suggest-symbol) + #+sbcl (sb-ext:package-does-not-exist #'suggest-package) + #+sbcl (sb-int:simple-reader-package-error #'suggest-symbol) + #+ (or) + (package-error #'suggest-package) + #+sbcl + (sb-pcl:class-not-found-error #'suggest-class)) + (funcall function)))) diff --git a/src/test-file.lisp b/src/test-file.lisp new file mode 100644 index 00000000..04800df8 --- /dev/null +++ b/src/test-file.lisp @@ -0,0 +1,135 @@ + +(cl:in-package :cl-user) + +(defpackage #:breeze.test-file + (:documentation "Parsing test files inspired by emacs' ERT's .erts files.") + (:use #:cl) + (:import-from #:alexandria + #:symbolicate + #:when-let + #:make-keyword) + (:import-from #:breeze.utils + #:whitespacep + #:trim-whitespace + #:with-collectors + #:with) + (:export #:read-spec-file)) + +(in-package #:breeze.test-file) + +(defun string-bool (string) + "If string is a representation of T or NIL, then coerce it." + (cond + ((string-equal string "nil") nil) + ((string-equal string "t") t) + (t string))) + +(defun part-delimiter-p (string) + (and string + (string= (trim-whitespace string) "=-="))) + +(defun end-delimiter-p (string) + (and string + (string= (trim-whitespace string) "=-=-="))) + + + +(defun read-spec-file (pathname) + (with + ((open-file (stream pathname)) + (collectors (tests parts)) + (let ((attributes (make-hash-table)) + (eof (gensym "eof")))) + (macrolet + ((push-char () `(write-char c out)))) + (labels + ((peek (&optional (peek-type t)) + (peek-char peek-type stream nil eof)) + (get-char () (read-char stream)) + (eofp (x) (eq eof x)) + (clean-attributes () (remhash :skip attributes)) + (trim-last-newline (string) + (let* ((end (1- (length string)))) + (if (char= #\Newline (char string end)) + (subseq string 0 end) + string))) + (read-comment (c) + (when (char= #\; c) + (read-line stream nil t))) + (read-string (string) + (loop :for c :across string + :do (char= c (get-char)))) + (read-test (c) + (when (char= #\= c) + (with-output-to-string (out) + (read-string #. (format nil "=-=~%")) + (loop :for line = (read-line stream) + :do (cond + ((part-delimiter-p line) + (push-parts (trim-last-newline (get-output-stream-string out)))) + ((end-delimiter-p line) + (push-parts (trim-last-newline (get-output-stream-string out))) + (push-tests `(,@(alexandria:hash-table-plist attributes) + :parts ,(drain-parts))) + (peek) ;; skip whitespaces + (clean-attributes) + (return-from read-test t)) + ((string= "\\=-=" line) + (write-string line out :start 1) + (write-char #\newline out)) + (t (write-string line out) + (write-char #\newline out))))))) + (read-attribute-name () + (make-keyword + (string-upcase + (with-output-to-string (out) + (loop :for c = (get-char) + :until (char= c #\:) + :do (write-char c out)))))) + (read-attribute-value () + (string-bool + (trim-whitespace + (with-output-to-string (out) + (loop + :for nl = nil :then (or (char= #\Linefeed c) + (char= #\Return c)) + :for c = (peek nil) + :until (or (eofp c) + (and nl (not (whitespacep c)))) + :do + ;; (format t "~%c = ~s nl = ~s" c nl) + (if (read-comment c) + (unread-char (setf c #\Return) stream) + (write-char (get-char) out))))))) + (read-attribute () + (let ((name (read-attribute-name)) + (value (read-attribute-value))) + (setf (gethash name attributes) value)))))) + (loop + :for c = (peek) + :repeat 250 ;; guard + :until (eofp c) + :for part = (or + (whitespacep c) + (read-comment c) + (read-test c) + (read-attribute)) + ;; :do (format t "~&~s" part) + ) + ;; (format t "~&Final: ~% ~{~s~%~}" (tests)) + (tests))) + +#++ +(defparameter *structural-editing-tests* + (read-spec-file + (asdf:system-relative-pathname + "breeze" "scratch-files/notes/strutural-editing.lisp"))) + + +#++ +(loop :for test :in *structural-editing-tests* + :do (format t "~&~a: ~a parts" + (getf test :name) + (length (getf test :parts))) + ;; :do (print ) + ) diff --git a/src/thread.lisp b/src/thread.lisp index 9e94d0db..3e6f3a48 100644 --- a/src/thread.lisp +++ b/src/thread.lisp @@ -25,6 +25,11 @@ t))) (bt:all-threads)))) +(defun find-threads-by-prefix (prefix &key (exclude-self-p t)) + (find-threads #'(lambda (thread) + (alexandria:starts-with-subseq prefix (bt:thread-name thread))) + exclude-self-p)) + (defun find-threads-by-name (name &key (exclude-self-p t)) (find-threads #'(lambda (thread) (string= (bt:thread-name thread) name)) diff --git a/src/utils.lisp b/src/utils.lisp index 28ee22fb..954eae62 100644 --- a/src/utils.lisp +++ b/src/utils.lisp @@ -2,35 +2,91 @@ (defpackage #:breeze.utils (:use :cl) (:documentation "Utilities") + (:import-from #:alexandria #:symbolicate) (:export + #:string-designator #:around - #:package-apropos #:optimal-string-alignment-distance #:optimal-string-alignment-distance* - #:walk - #:walk-car - #:walk-list + #:repeat-string + #:split-by-newline #:indent-string #:remove-indentation #:print-comparison #:summarize - #:breeze-relative-pathname #:+whitespaces+ + #:trim-whitespace #:whitespacep - #:stream-size - #:read-stream-range - #:symbol-package-qualified-name + #:symbol-package-qualified-name) + (:export + #:walk + #:walk-car + #:walk-list + #:package-apropos) + (:export #:before-last - #:find-version-control-root - #:find-asdf-in-parent-directories #:subseq-displaced - #:length>1?)) + #:length>1? + #:with-collectors) + (:export + #:stream-size + #:read-stream-range) + (:export + #:breeze-relative-pathname + #:find-version-control-root + #:find-asdf-in-parent-directories)) (in-package #:breeze.utils) ;;; Other +(defmacro with (clauses &body body) + (loop + :for clause :in (reverse clauses) + :for (first . rest) = (if (listp clause) + clause + (list clause)) + :for symbol-package = (symbol-package first) + :for symbol-name = (if (or + (eq 'with first) + (string= "COMMON-LISP" + (package-name symbol-package))) + (symbol-name first) + (concatenate 'string "WITH-" (symbol-name first))) + :do + (multiple-value-bind (with status) + (find-symbol symbol-name symbol-package) + (cond + ((null with) + (error "Can't find symbol ~A:WITH-~A" (package-name symbol-package) symbol-name)) + ((eq 'with first) + (setf body `((let ((,(first rest) ,@(when (rest rest) + `((with ,(rest rest)))))) + ,@body)))) + ((and (not (eq *package* symbol-package)) (eq :internal status)) + (error "The symbol ~s is interal to ~s" with symbol-package)) + (t (setf body `((,with ,@rest ,@body))))))) + (car body)) + + +;; TODO make tests +#++ +(progn + (with + ((open-file (in "my-file"))) + test) + + (with + ((output-to-string (out))) + test) + + (with + ((let ((y 42))) + (with x (output-to-string (out) + (format out "hello ~d" y)))) + x)) + ;; TODO I don't think I use this (defun walk (tree fn &optional (recurse-p (constantly t))) "Walk a tree and call fn on every elements" @@ -64,218 +120,6 @@ :test #'string-equal)) (list-all-packages))) - -;;; String stuff - -(defun optimal-string-alignment-distance (vec-a vec-b) - "Compute an edit distance between two vector." - (let* ((m (length vec-a)) - (n (length vec-b)) - (diff-0 (make-array (list (1+ n)) :element-type 'integer)) - (diff-1 (make-array (list (1+ n)) :element-type 'integer)) - (diff-2 (make-array (list (1+ n)) :element-type 'integer))) - - (loop :for i :upto n :do - (setf (aref diff-1 i) i)) - (setf (aref diff-0 0) 1) - - (flet ((a (index) (aref vec-a (1- index))) - (b (index) (aref vec-b (1- index))) - (diff-0 (index) (aref diff-0 index)) - (diff-1 (index) (aref diff-1 index)) - (diff-2 (index) (aref diff-2 index))) - (loop :for i :from 1 :upto m :do - (loop :for j :from 1 :upto n - :for cost = (if (eq (a i) (b j)) 0 1) ;; aka substitution-cost - :do - (setf (aref diff-0 j) (min - (1+ (diff-1 j)) ;; deletion - (1+ (diff-0 (1- j))) ;; insertion - (+ cost (diff-1 (1- j))) ;; substitution - )) - ;; transposition - (when (and (< 1 i) (< 1 j) - (eq (a i) (b (1- j))) - (eq (a (1- i)) (b j))) - (setf (aref diff-0 j) (min (diff-0 j) - (+ cost (diff-2 (- j 2))))))) - (when (/= m i) - (let ((tmp diff-2)) - (setf diff-2 diff-1 - diff-1 diff-0 - diff-0 tmp - (aref diff-0 0) (1+ i))))) - (diff-0 n)))) - -(defun optimal-string-alignment-distance* (vec-a vec-b max-distance) - "Compute an edit distance between two vector. Stops as soon as -max-distance is reached, returns nil in that case." - (unless (> (abs (- (length vec-a) - (length vec-b))) - max-distance) - (let* ((m (length vec-a)) - (n (length vec-b)) - (diff-0 (make-array (list (1+ n)) :element-type 'integer)) - (diff-1 (make-array (list (1+ n)) :element-type 'integer)) - (diff-2 (make-array (list (1+ n)) :element-type 'integer))) - - (loop :for i :upto n :do - (setf (aref diff-1 i) i)) - (setf (aref diff-0 0) 1) - - (flet ((a (index) (aref vec-a (1- index))) - (b (index) (aref vec-b (1- index))) - (diff-0 (index) (aref diff-0 index)) - (diff-1 (index) (aref diff-1 index)) - (diff-2 (index) (aref diff-2 index))) - (loop - :for min-distance = nil - :for i :from 1 :upto m :do - (loop :for j :from 1 :upto n - ;; aka substitution-cost - :for cost = (if (eq (a i) (b j)) 0 1) - :do - (setf (aref diff-0 j) (min - ;; deletion - (1+ (diff-1 j)) - ;; insertion - (1+ (diff-0 (1- j))) - ;; substitution - (+ cost (diff-1 (1- j))))) - ;; transposition - (when (and (< 1 i) (< 1 j) - (eq (a i) (b (1- j))) - (eq (a (1- i)) (b j))) - (setf (aref diff-0 j) (min (diff-0 j) - (+ cost (diff-2 (- j 2)))))) - (when (or (null min-distance) - (> min-distance (diff-0 j))) - ;; (format *debug-io* "~&new min-distance ~s" min-distance) - (setf min-distance (diff-0 j)))) - ;; (format *debug-io* "~&~s ~s" i diff-0) - (when (and (> i 1) - (>= min-distance max-distance)) - #+ (or) - (format *debug-io* "~&min-distance ~s > max-distance ~s" - min-distance max-distance) - (return-from optimal-string-alignment-distance*)) - (when (/= m i) - (let ((tmp diff-2)) - (setf diff-2 diff-1 - diff-1 diff-0 - diff-0 tmp - (aref diff-0 0) (1+ i))))) - (diff-0 n))))) - -(defun indent-string (indentation string) - "Prepend INDENTATION spaces at the beginning of each line in STRING." - (check-type indentation (integer 0)) - (with-input-from-string (input string) - (with-output-to-string (output) - (loop :for line = (read-line input nil nil) - :while line - :do (format output "~a~a~%" (str:repeat indentation " ") line))))) - -#| -(indent-string 4 (format nil "a~%b")) -" a -b -" -|# - -(defun leading-whitespaces (string) - (with-input-from-string (input string) - ;; Skip the first line - (when (read-line input nil nil) - (loop :for line = (read-line input nil nil) - :while line - :for leading-whitespaces = (position-if-not #'whitespacep line) - :when leading-whitespaces - :minimize leading-whitespaces)))) - -(defun remove-indentation (string) - (let ((indentation (leading-whitespaces string))) - (with-input-from-string (input string) - (with-output-to-string (output) - (loop :for line = (read-line input nil nil) - :while line - :for leading-whitespaces = (position-if-not #'whitespacep line) - :if (and leading-whitespaces - (>= leading-whitespaces indentation)) - :do (write-string (subseq-displaced line indentation) output) - :else - :do (write-string line output) - :do (write-char #\newline output)))))) - -;; TODO IIRC this function sucked, I think it might just need some -;; *print- variables set to the right thing... TO TEST -(defun print-comparison (stream string1 string2) - "Print two (close) string in a way that the difference are easier to see." - (let* ((mismatch (mismatch string1 string2))) - (format stream "~&~a~%~a|~%~a" - string1 - (if (null mismatch) - "" - (str:repeat mismatch "=")) - string2))) - -#| -(print-comparison nil "abc" "adc") - -(print-comparison nil "abce" "abcd") - -(print-comparison nil -(string-downcase 'system-files) -(string-downcase 'sytsem-files)) -"system-files -==| -sytsem-files" -|# - - -(defun summarize (string) - "Keep only the first sentence, remove parenthesis." - (cl-ppcre:regex-replace-all - "\\([^)]*\\) *" - (alexandria:if-let (position (position #\. string)) - (subseq string 0 position) - string) - "")) - - -(defun around (string position &optional (around 10)) - "Returns part of STRING, from POSITIONITION - AROUND to POSITIONITION + AROUND." - (let* ((min-size (1+ (* 2 around))) - (before (- position around)) - (start (max 0 before)) - (after (+ start min-size)) - (end (min (length string) after)) - (start (max 0 (min start (- end min-size)))) - (ellipsis-left (max 0 (min 3 start))) - (ellipsis-right (max 0 (min 3 (- (length string) end))))) - (with-output-to-string (out) - (loop :for i :below ellipsis-left :do (write-char #\. out)) - (write-string string out :start start :end end) - (loop :for i :below ellipsis-right :do (write-char #\. out))))) - - -(alexandria:define-constant +whitespaces+ - #. (coerce '(#\Space #\Newline #\Backspace #\Tab #\Linefeed #\Page #\Return - #\Rubout) - 'string) - :test 'equal) - -(defun whitespacep (char) - "Is CHAR a whitespace?" - (position char +whitespaces+ :test #'char=)) - - -(defun symbol-package-qualified-name (symbol) - "Given a SYMBOL return a string of the form package:symbol." - (let ((*print-escape* t) - (*package* (find-package "KEYWORD"))) - (prin1-to-string symbol))) - ;;; Stream stuff @@ -305,10 +149,15 @@ sytsem-files" (defun breeze-relative-pathname (pathname) "Returns a pathname relative to breeze's location." - (if (cl-fad:pathname-relative-p pathname) + (if (uiop:relative-pathname-p pathname) (asdf:system-relative-pathname :breeze pathname) pathname)) +;; TODO This is kinda like "locate-dominating-file" in emacs, it might +;; be a better name? +;; +;; TODO FIXME I got the condition "Invalid use of :BACK after +;; :ABSOLUTE." when I called this on breeze's directory (defun find-witness-in-parent-directories (starting-path witness &key (test #'uiop:probe-file*)) "Search for a directory called WITNESS in current and parent @@ -365,3 +214,76 @@ should be easy to add." :finally (return (when (cdr rest) (car rest))))) + +(defmacro with-collectors ((&rest collectors) &body body) + "Introduce a set of list with functions to push , get, set, etc those +lists." + (let* ((variables (mapcar #'(lambda (x) (gensym (symbol-name x))) collectors)) + (labels (loop :for collector :in collectors + :for v :in variables + :for push = (symbolicate 'push- collector) + :for set = (symbolicate 'set- collector) + :for drain = (symbolicate 'drain- collector) + :append `((,push (x) + (unless (car ,v) + (setf ,v nil)) + (let ((new-tail (cons x nil))) + (if ,v + (setf (cddr ,v) new-tail + (cdr ,v) new-tail) + (setf ,v (cons new-tail new-tail)))) + x) + (,set (&optional x) + (unless ,v + (setf ,v (cons nil nil))) + (setf (car ,v) (copy-list x) + (cdr ,v) (last (car ,v))) + x) + ((setf ,collector) (new-value) (,set new-value)) + (,drain () (,collector nil)) + (,collector (&optional (new-value nil new-value-p)) + (if new-value-p + (prog1 (when ,v (car ,v)) + (,set new-value)) + (when ,v (car ,v)))))))) + `(let ,variables + (labels + ,labels + (declare (ignorable ,@(loop :for (label . rest) :in labels + :collect `(function ,label)))) + ,@body)))) + +;; TODO make tests +#++ +(progn + (with-collectors (x) + (x '(32)) + (x)) + + (with-collectors (x) + (x '(32))) + + (with-collectors (x) + (push-x 0) + (push-x 1) + (push-x 3) + (x)) + + (with-collectors (x y) + (push-x 0) + (push-y (copy-list (x))) + (push-y 4) + + (push-x 1) + (x '(a b c)) + ;; == (setf (x) '(a b c)) + ;; == (set-x '(a b c)) + + (push-x 2) + (push-x 3) + + (list (x) (y)) + ;; == (mapcar #'funcall (list #'x #'y)) + ) + ;; => ((A B C 2 3) ((0) 4)) + ) diff --git a/src/xref.lisp b/src/xref.lisp index 46aa47f9..da46a762 100644 --- a/src/xref.lisp +++ b/src/xref.lisp @@ -6,7 +6,6 @@ #:calls-who ;; Utilities #:find-packages-by-prefix - #:find-packages-by-regex ;; Symbol inspection #:generic-method-p #:specialp @@ -28,17 +27,6 @@ (package-name package))) :collect package)) -(defun find-packages-by-regex (regex &optional (case-insensitive-p t)) - "Find all packages whose name match the regex (case insensitive by default)." - (loop - :with scanner = (cl-ppcre:create-scanner regex :case-insensitive-mode - case-insensitive-p) - :for package :in (list-all-packages) - :when (cl-ppcre:scan scanner - (string-downcase - (package-name package))) - :collect package)) - (defun generic-method-p (symbol) "Returns T if SYMBOL designates a generic method" (and (fboundp symbol) diff --git a/tests/analysis.lisp b/tests/analysis.lisp new file mode 100644 index 00000000..dd665c4f --- /dev/null +++ b/tests/analysis.lisp @@ -0,0 +1,415 @@ +(defpackage #:breeze.test.analysis + (:documentation "Tests for the package breeze.analysis") + (:use #:cl #:breeze.analysis) + (:import-from #:parachute + #:define-test + #:define-test+run + #:is + #:true + #:false + #:of-type) + ;; importing unexported symbols + (:import-from #:breeze.pattern + #:termp + #:term-name) + ;; importing unexported symbols + (:import-from #:breeze.analysis + #:malformed-if-node-p)) + +(in-package #:breeze.test.analysis) + + +;;; Integrating pattern.lisp and lossless-parser.lisp + +(defun normalize-bindings (bindings) + "This is only to make it easier to compare the bindings in the tests." + (or (eq t bindings) + (alexandria:alist-plist + (sort (loop :for (key . value) :in bindings + :collect (cons (if (termp key) + (term-name key) + key) + value)) + #'string< + :key #'car)))) + +(defun test-match-parse (pattern string &optional skip-whitespaces-and-comments) + (let* ((state (parse string)) + (*match-skip* (when skip-whitespaces-and-comments + #'whitespace-or-comment-node-p)) + (bindings (match (compile-pattern pattern) state)) + (bindings (normalize-bindings bindings))) + (values bindings state))) + + + +(define-test+run "match pattern nil and (nil) against parse trees" + ;; pattern nil + (loop + :for skip-p :in '(nil t) + :do (progn + ;; TODO I'm not sure what should be the right things + ;; - on one hand the parse tree _is_ nil + ;; - on the other hand, (read "") would error + ;; (false (test-match-parse nil "")) + (false (test-match-parse nil " " skip-p)) + (false (test-match-parse nil "; hi" skip-p)) + (false (test-match-parse nil "#| hi |#" skip-p)) + (false (test-match-parse nil "nil" skip-p)) + (false (test-match-parse nil "NIL" skip-p)) + (false (test-match-parse nil "nIl" skip-p)) + (false (test-match-parse nil "cl:nil" skip-p)) + (false (test-match-parse nil "cl::nil" skip-p)) + (false (test-match-parse nil "common-lisp:nil" skip-p)) + (false (test-match-parse nil "common-lisp::nil" skip-p)) + (false (test-match-parse nil "common-lisp-user::nil" skip-p)) + (false (test-match-parse nil "common-lisp-user:nil" skip-p)))) + (progn + (false (test-match-parse '(nil) "")) + (false (test-match-parse '(nil) " ")) + (false (test-match-parse '(nil) "; hi")) + (false (test-match-parse '(nil) "#| hi |#")) + (true (test-match-parse '(nil) "nil")) + (true (test-match-parse '(nil) "NIL")) + (true (test-match-parse '(nil) "nIl")) + (true (test-match-parse '(nil) "cl:nil")) + (true (test-match-parse '(nil) "cl::nil")) + (true (test-match-parse '(nil) "common-lisp:nil")) + (true (test-match-parse '(nil) "common-lisp::nil")) + ;; TODO For now we don't check _all_ the package a symbol might be + ;; part of + (false (test-match-parse '(nil) "common-lisp-user::nil")) + (false (test-match-parse '(nil) "common-lisp-user:nil"))) + (progn + (false (test-match-parse '(nil) "" t)) + (false (test-match-parse '(nil) " " t)) + (false (test-match-parse '(nil) "; hi" t)) + (false (test-match-parse '(nil) "#| hi |#" t)) + + (true (test-match-parse '(nil) " nil " t)) + + (true (test-match-parse '(nil) " #| t |# NIL" t)) + (true (test-match-parse '(nil) " nIl" t)) + (true (test-match-parse '(nil) " ;; look ma! + cl:nil" t)) + (true (test-match-parse '(nil) " #||# cl::nil" t)) + (true (test-match-parse '(nil) " #|;;|# common-lisp:nil " t)) + (true (test-match-parse '(nil) " common-lisp::nil " t)) + ;; TODO For now we don't check _all_ the package a symbol might be + ;; part of + (false (test-match-parse '(nil) "common-lisp-user::nil" t)) + (false (test-match-parse '(nil) "common-lisp-user:nil" t)))) + +(define-test+run "match the patterns t and (t) against parse trees" + ;; These should return nil because we're trying to match 1 symbol + ;; against a list of nodes (even if that list is empty). + (loop + :for skip-p :in '(nil t) + :do (progn + (false (test-match-parse t "" skip-p)) + (false (test-match-parse t " " skip-p)) + (false (test-match-parse t "; hi" skip-p)) + (false (test-match-parse t "#| hi |#" skip-p)) + (false (test-match-parse t "t" skip-p)) + (false (test-match-parse t "T" skip-p)) + (false (test-match-parse t "t" skip-p)) + (false (test-match-parse t "cl:t" skip-p)) + (false (test-match-parse t "cl::t" skip-p)) + (false (test-match-parse t "common-lisp:t" skip-p)) + (false (test-match-parse t "common-lisp::t" skip-p)) + ;; TODO For now we don't check _all_ the package a symbol might be + ;; part of + (false (test-match-parse t "common-lisp-user::t" skip-p)) + (false (test-match-parse t "common-lisp-user:t" skip-p)) + (progn + (false (test-match-parse t " t" skip-p)) + (false (test-match-parse t "t " skip-p)) + (false (test-match-parse t "t ; hi" skip-p)) + (false (test-match-parse t "; t " skip-p)) + (false (test-match-parse t "t #| hi |#" skip-p)) + (false (test-match-parse t "#| t |#" skip-p)) + (false (test-match-parse t "#| hi |# t" skip-p))))) + (progn + ;; These should return the same thing whether the match is + ;; skipping comments and whitespaces or not. + (loop + :for skip-p :in '(nil t) + :do (progn + (false (test-match-parse '(t) "" skip-p)) + (false (test-match-parse '(t) " " skip-p)) + (false (test-match-parse '(t) "; hi" skip-p)) + (false (test-match-parse '(t) "#| hi |#" skip-p)) + (true (test-match-parse '(t) "t" skip-p)) + (true (test-match-parse '(t) "T" skip-p)) + (true (test-match-parse '(t) "t" skip-p)) + (true (test-match-parse '(t) "cl:t" skip-p)) + (true (test-match-parse '(t) "cl::t" skip-p)) + (true (test-match-parse '(t) "common-lisp:t" skip-p)) + (true (test-match-parse '(t) "common-lisp::t" skip-p)) + ;; TODO For now we don't check _all_ the package a symbol might be + ;; part of + (false (test-match-parse '(t) "common-lisp-user::t" skip-p)) + (false (test-match-parse '(t) "common-lisp-user:t" skip-p)))) + (progn + (false (test-match-parse '(t) "t ; hi")) + (false (test-match-parse '(t) "t ")) + (false (test-match-parse '(t) "t #| hi |#")) + (true (test-match-parse '(t) "t ; hi" t)) + (true (test-match-parse '(t) "t " t)) + (true (test-match-parse '(t) "t #| hi |#" t)) + (true (test-match-parse '(t) " t" t)) + (false (test-match-parse '(t) "; '(t) " t)) + (false (test-match-parse '(t) "#| t |#" t)) + (true (test-match-parse '(t) "#| hi |# t" t))) + (true (test-match-parse '((t)) " (t) " t)))) + +;; TODO test pattern 1 +;; TODO test pattern 'x +;; TODO test pattern :x +;; TODO test pattern "x" +;; TODO test pattern "some-node" (I'll have to think about the syntax) + +(define-test+run "match terms against parse trees" + (progn + (is equalp (list :?x nil) (test-match-parse :?x "")) + (is equalp (list :?x nil) (test-match-parse :?x "" t)) + (is equalp + (list :?x (list (token 0 1))) + (test-match-parse :?x "x")) + (is equalp + (list :?x (list (whitespace 0 1) (token 1 2))) + (test-match-parse :?x " x")) + (is equalp + (list :?x (list (whitespace 0 1) (token 1 2))) + (test-match-parse :?x " x" t))) + (progn + (false (test-match-parse '(:?x) "")) + (false (test-match-parse '(:?x) "" t)) + (is equalp + (list :?x (token 0 1)) + (test-match-parse '(:?x) "x")) + (false (test-match-parse '(:?x) " x")) + (is equalp + (list :?x (token 1 2)) + (test-match-parse '(:?x) " x" t)) + (is equalp + (list :?x (parens 0 4 (list (token 1 3)))) + (test-match-parse '(:?x) "(42)")) + (is equalp + (list :?x (token 1 3)) + (test-match-parse '((:?x)) "(42)")))) + +(define-test+run "match vector against parse trees" + (false (test-match-parse 'x "x")) + (true (test-match-parse #(x) "x")) + (true (test-match-parse '((x)) "(x)"))) + + +;;; Basic tree inspection + +#++ ;; Sanity-check +(mapcar #'read-from-string + '("in-package" + "common-lisp:in-package" + "cl:in-package" + "cl-user::in-package" + "common-lisp-user::in-package")) + +(defun test-in-package-node-p (string) + (let* ((state (parse string)) + (node (first (tree state)))) + ;; The funky reader macro and quasiquote is to fuck with slime and + ;; sly's regex-based search for "(in-package". Without this the + ;; rest of the file is evaluated in cl-user by slime and sly. + (let ((package-designator-node + #.`(,'in-package-node-p state node))) + (when package-designator-node + (node-content state package-designator-node))))) + +(define-test+run in-package-node-p + (is equal "x" (test-in-package-node-p "(in-package x)")) + (is equal ":x" (test-in-package-node-p "(in-package :x)")) + (is equal "#:x" (test-in-package-node-p "(in-package #:x)")) + (is equal "\"x\"" (test-in-package-node-p "(in-package \"x\")")) + (is equal "x" (test-in-package-node-p "( in-package x )")) + (is equal "x" (test-in-package-node-p "( in-package #| ∿ |# x )")) + (is equal "x" (test-in-package-node-p "(cl:in-package x)")) + (is equal "x" (test-in-package-node-p "(cl::in-package x)")) + (is equal "42" (test-in-package-node-p "(cl::in-package 42)")) + ;; TODO ? Not sure it's worth it lol... + ;; (is equal "x" (test-in-package-node-p "('|CL|::|IN-PACKAGE| x)")) + (null (test-in-package-node-p "(cl:)"))) + +(defun test-malformed-if-node-p (string) + (let* ((state (parse string)) + (node (first (tree state)))) + (malformed-if-node-p state node))) + +#++ ;; WIP +(define-test+run malformed-if-node-p + (false (test-malformed-if-node-p "(if a b c)")) + (true (test-malformed-if-node-p "(if a b c d)"))) + + + +(define-test find-node + (is equal + '((whitespace . 0) (parens . 1) (parens . 1) (parens . 1) (parens . 1) + (parens . 1) (parens . 1) (parens . 1) (parens . 1) (whitespace . 2)) + (loop :with input = " ( loop ) " + :with state = (parse input) + :for i :from 0 :below (length input) + :for path = (find-node i (tree state)) + :collect (cons (node-type (car path)) (cdr path))))) + +(define-test find-path-to-position + (is equalp + '((whitespace) + (parens whitespace) + (parens whitespace) + (parens token) + (parens token) + (parens token) + (parens token) + (parens whitespace) + (parens) + (whitespace)) + (loop :with input = " ( loop ) " + :with state = (parse input) + :for i :from 0 :below (length input) + :for path = (find-path-to-position state i) + :collect + (mapcar (lambda (path) + (node-type (car path))) + path) + #++(list i (length path))))) + + +;;; Fixing formatting issues... + +(defun parens-has-leading-whitespaces-p (node) + (and (parens-node-p node) + (whitespace-node-p (first (node-children node))))) + +(defun parens-has-trailing-whitespaces-p (node) + (and (parens-node-p node) + (whitespace-node-p (alexandria:lastcar (node-children node))))) + +(defun cdr-if (condition list) + (if condition (cdr list) list)) + +(defun butlast-if (condition list) + (if condition (butlast list) list)) + +(defun fix-trailing-whitespaces-inside-parens (node) + (let ((first-child (parens-has-leading-whitespaces-p node)) + (last-child (parens-has-trailing-whitespaces-p node))) + (if (or first-child last-child) + (copy-parens + node + :children (butlast-if + last-child + (cdr-if first-child (node-children node)))) + node))) + + +(defun test-remove-whitespaces (input output) + (let* ((input (format nil input)) + (output (format nil output)) + (state (parse input))) + (breeze.kite:is + :comparator 'string= + :form `(unparse ,state nil 'fix-trailing-whitespaces-inside-parens) + :got (unparse state nil 'fix-trailing-whitespaces-inside-parens) + :expected output))) + +(define-test+run remove-whitespaces + (test-remove-whitespaces "( )" "()") + (test-remove-whitespaces "(~%~%~%)" "()") + (test-remove-whitespaces "( ) " "() ") + (test-remove-whitespaces " ( ) " " () ") + ;; TODO handle indentation levels! + ;; (test-remove-whitespaces "(;;~% )" "(;;~% )") + (test-remove-whitespaces "( x)" "(x)") + (test-remove-whitespaces "( x )" "(x)")) + + + +;;; Testing the linter + +(defun test-lint (buffer-string) + (lint :buffer-string buffer-string)) + + +(define-test+run lint + (false (test-lint "")) + (false (test-lint ";; ")) + (is equal '((0 2 :error "Syntax error")) (test-lint "#+")) + (false (test-lint "(in-package :cl-user)")) + (false (test-lint "(in-package 42)")) + (is equal '((0 56 :warning + "Package PLEASE-DONT-DEFINE-A-PACKAGE-WITH-THIS-NAME is not currently defined.")) + (test-lint "(in-package please-dont-define-a-package-with-this-name)")) + #++ ;; TODO check if "in-package" is NOT quoted + (progn + (false (test-lint "'(in-package :PLEASE-DONT-DEFINE-A-PACKAGE-WITH-THIS-NAME)")) + (false (test-lint "`(in-package :PLEASE-DONT-DEFINE-A-PACKAGE-WITH-THIS-NAME)"))) + (is equalp + '((1 3 :warning "Extraneous whitespaces.")) + (test-lint "( )")) + (is equalp + '((2 4 :warning "Extraneous internal whitespaces.")) + (test-lint "(x y)")) + (is equalp + '((3 4 :warning "Extraneous trailing whitespaces.") + (1 2 :warning "Extraneous leading whitespaces.")) + (test-lint "( x )"))) + +#++ ;; Syntax errors +(progn + (test-lint "(") + (test-lint "')") + (test-lint "'1") + (test-lint "..") + (test-lint "( . )") + (test-lint "( a . )") + (test-lint "( a . b . c )") + (test-lint "( a . b c )") + (test-lint "#1=") + (test-lint "#1=#1#") + (test-lint "(;;)") + (test-lint "::") + (test-lint "x::") + (test-lint "::x") + (test-lint "a:b:c") + (test-lint "a:::b") + (test-lint "b:") + (test-lint "b::") + (test-lint "\\") + (test-lint "\\\\") ;; Should be OK + (test-lint "|") + (test-lint "'") + (test-lint "(#++;;)") + (test-lint "(#+;;)") + (test-lint "(#)") + (test-lint ",") + (test-lint ",@") + (test-lint "`,@x") + (test-lint "`(a b . ,@x)") ; "has undefined consequences" + ;; TODO "unknown character name" + (test-lint "1/0") + ;; TODO check for invalid radix + (test-lint "#|") + (test-lint "#c(a b c d)")) + +;; Formatting Style +#++ +(progn + (test-lint "#+ ()") + (test-lint " ; this is ok") + (test-lint ";I don't like this") + (test-lint "; not that")) + +#++ ;; Style warnings +(progn + (test-lint "like::%really")) diff --git a/tests/breeze-test.el b/tests/breeze-test.el index 50d45e62..4b386d72 100644 --- a/tests/breeze-test.el +++ b/tests/breeze-test.el @@ -1,43 +1,87 @@ -;;; These are drafts of tests for breeze.el +(require 'ert) -(breeze-eval "(+ 1 2)") -;; ("" "3 (2 bits, #x3, #o3, #b11)") +(defun breeze--xor (a b) + (or (and a (not b)) + (and (not a) b))) -;; Should error -;; (breeze-eval "") +(ert-deftest test/breeze--xor () + (should (equal '(nil t t nil) + (mapcar (lambda (args) + (apply 'breeze--xor args)) + '((nil nil) + (nil t) + (t nil) + (t t)))))) -(breeze-eval "(error \"oups\")") - -(and - (eq t (breeze-eval-predicate "t")) - (eq t (breeze-eval-predicate "T")) - (eq nil (breeze-eval-predicate "nil")) - (eq nil (breeze-eval-predicate "NIL"))) - - -(breeze-interactive-eval "(read)") + -(breeze-interactive-eval "(error \"oupsie\")") +(ert-deftest test/breeze-%symbolicate () + (should (eq 'sly (breeze-%symbolicate2 "sly"))) + (should (eq 'sly (breeze-%symbolicate2 'sly))) + (should (eq 'slime (breeze-%symbolicate2 "slime"))) + (should (eq 'slime (breeze-%symbolicate2 'slime))) + (should (eq 'sly-eval (breeze-%symbolicate2 'sly "eval"))) + (should (eq 'slime-eval (breeze-%symbolicate2 'slime "eval"))) + (should (eq 'slime-connected-hook + (breeze-%symbolicate2 'slime "connected-hook")))) -(breeze-check-if-connected-to-listener) +;; TODO true only if connected! +(ert-deftest test/breeze-connection () + (should (breeze--xor + (breeze-sly-connected-p) + (breeze-slime-connected-p))) + (should (eq t (breeze-check-if-connected-to-listener)))) + +(ert-deftest test/breeze-eval () + ;; Integers + (should (= (breeze-eval "(+ 1 2)") 3)) + ;; Strings + + (should (string= (breeze-eval "\"hi\"") "hi")) + ;; Symbols + (should (eq (breeze-eval "cl:t") t)) + (should (eq (breeze-eval "cl:nil") nil)) + (should (eq t (breeze-eval-predicate "t"))) + (should (eq t (breeze-eval-predicate "T"))) + (should (eq nil (breeze-eval-predicate "nil"))) + (should (eq nil (breeze-eval-predicate "NIL")))) + +;; TODO Figure out how to evaluate something without triggering the debugger when an error occurs +;; (ert-deftest breeze-eval-empty-string () +;; :expected-result :failed +;; (breeze-eval "")) + +;; (let ((slime-event-hooks (list (lambda (event) +;; (message "Event: %S" (list (car event) +;; (length (cdr event)))) +;; nil)))) +;; (breeze-eval "(error \"oups\")")) +;; (breeze-eval "(read)") -(breeze-ensure-breeze t) - -(breeze-init) -(breeze-validate-if-package-exists "CL") +(ert-deftest test/breeze-relative-path () + (should (file-exists-p (breeze-relative-path))) + (should (file-exists-p (breeze-relative-path "src/"))) + (should (file-exists-p (breeze-relative-path "src/breeze.el"))) + (should (file-exists-p (breeze-relative-path "src/ensure-breeze.lisp")))) + +(ert-deftest test/breeze-init () + (should (eq t (breeze-validate-if-package-exists "CL"))) + (should (eq nil (breeze-validate-if-package-exists "this package probably doesn't exists")))) ;; t -(breeze-validate-if-package-exists "this package probably doesn't exists") -;; nil -(breeze-validate-if-breeze-package-exists) +;; TODO only after (breeze-ensure) +;; (should (eq t (breeze-validate-if-breeze-package-exists))) + +;; (should (eq t (breeze-ensure))) -(breeze-system-definition) +(ert-deftest test/breeze-intergration () + (ert-test-erts-file "breeze.erts")) diff --git a/tests/breeze.erts b/tests/breeze.erts new file mode 100644 index 00000000..0bacff5f --- /dev/null +++ b/tests/breeze.erts @@ -0,0 +1,31 @@ + +Point-Char: | + +Name: insert-in-package-cl-user +Code: breeze-insert-in-package-cl-user + +=-= +| +=-= +(cl:in-package #:cl-user) +=-=-= + + +;; WIP +;; Name: insert-defun +;; Code: breeze-insert-defun + +;; =-= +;; | +;; =-= +;; (defun a (b c) +;; ) +;; =-=-= + + +;; Corrections of typos +;; =-= +;; |(lost 1 2 3) +;; =-= +;; |(list 1 2 3) +;; =-=-= diff --git a/tests/command.lisp b/tests/command.lisp index 5d47f3fe..d5b92f9a 100644 --- a/tests/command.lisp +++ b/tests/command.lisp @@ -162,27 +162,27 @@ N.B. \"Requests\" are what the command returns. \"inputs\" are answers to those ;; TODO (define-test message) -(define-test context-buffer-string +(define-test buffer-string (is string= "asdf" - (context-buffer-string + (buffer-string (alexandria:plist-hash-table '(buffer-string "asdf"))))) ;; TODO -(define-test context-buffer-name) +(define-test buffer-name) ;; TODO -(define-test context-buffer-file-name) +(define-test buffer-file-name) ;; TODO -(define-test context-point) +(define-test point) ;; TODO -(define-test context-point-min) +(define-test point-min) ;; TODO -(define-test context-point-max) +(define-test point-max) ;; TODO Test augment-context-by-parsing-the-buffer #++ diff --git a/tests/documentation.lisp b/tests/documentation.lisp index 591fa3a9..c6492179 100644 --- a/tests/documentation.lisp +++ b/tests/documentation.lisp @@ -51,4 +51,6 @@ (define-test generate-documentation - (breeze.documentation::generate-documentation)) + (with-output-to-string (*trace-output*) + (breeze.documentation::generate-documentation) + (breeze.report::render 'breeze))) diff --git a/tests/egraph.lisp b/tests/egraph.lisp new file mode 100644 index 00000000..a8142293 --- /dev/null +++ b/tests/egraph.lisp @@ -0,0 +1,755 @@ +(defpackage #:breeze.test.egraph + (:documentation "Tests for the package breeze.egraph.") + (:use #:cl #:breeze.egraph) + (:import-from #:parachute + #:define-test+run + #:define-test + #:is + #:true + #:false + #:fail) + (:import-from #:breeze.egraph + #:map-stream + #:map-egraph + #:stream-eclass + #:stream-equivalent-eclasses)) + +(in-package #:breeze.test.egraph) + +(define-test+run eclass + (let ((eclass (make-eclass 42 '(x)))) + (is = 42 (id eclass)) + (is equalp #(x) (enodes eclass))) + (let ((eclass (make-eclass 43 '(a b c)))) + (is = 43 (id eclass)) + (is equalp #(a b c) (enodes eclass))) + (let ((eclass (make-eclass 44 '(x) 'y))) + (is = 44 (id eclass)) + (is equalp #(x) (enodes eclass)) + ;; N.B. we use the symbol Y, but really "parents" is supposed to + ;; be a hash-table. + (is eq 'y (parents eclass)))) + +(define-test+run "add enode(s) to egraph" + (let* ((egraph (make-egraph)) + (enode 'x) + ;; Adding the e-node to the e-graph + (id (egraph-add-enode egraph enode)) + ;; Looking up the newly created e-class by the e-node + (eclass (eclass egraph id))) + ;; The first e-class we add should have the id 0 + (is = 0 id) + ;; Verifying that the newly created e-class contains the e-node + (is eq enode (aref (enodes eclass) 0)) + ;; Verifying the e-node's e-class + (is = id (eclass-id egraph enode))) + ;; Here, we add the e-node 'x to an e-graph that already contains it. + (let ((egraph (make-egraph))) + (egraph-add-enode egraph 'x) + (let ((id (egraph-add-enode egraph 'x))) + ;; The first e-class we add should have the id 0 + (is = 0 id) + (is = 1 (length (union-find egraph))) + (is = 1 (hash-table-count (eclasses egraph))) + (is = 1 (hash-table-count (eclasses egraph))))) + ;; Here, we add the same _FORM_ twice + (let* ((egraph (make-egraph))) + (add-form egraph '(+ 1 2)) + (add-form egraph '(+ 1 2)) + (is = 3 (length (union-find egraph))) + (is = 3 (hash-table-count (eclasses egraph))) + (is = 3 (hash-table-count (eclasses egraph)))) + (let* ((egraph (make-egraph))) + (add-form egraph '(+ x y)) + (add-form egraph '(+ x 2)) + (add-form egraph '(+ y y)) + ;; 3 distinct forms + 3 disctinct atoms = 6 + (is = 6 (length (union-find egraph))) + (is = 6 (hash-table-count (eclasses egraph))) + (is = 6 (hash-table-count (eclasses egraph))))) + + + +(define-test+run "enode<" + ;; eq + (progn + (false (enode< #1=#() #1#)) + (false (enode< #2=1 #2#)) + (false (enode< 'x 'x))) + ;; malformed enodes + (progn + (false (enode< #() #())) + (true (enode< #(x) #(y))) + (false (enode< #(x) #(x))) + (false (enode< #(y) #(x)))) + ;; proper enodes with children + (progn + (false (enode< #() #())) + (true (enode< #(x 0) #(y 1))) + (false (enode< #(x 0) #(x 0))) + (false (enode< #(y 1) #(x 0)))) + ;; symbols + (progn + (true (enode< 'x 'y)) + (false (enode< 'y 'x))) + ;; symbols v.s. vectors + (progn + (true (enode< 'x #())) + (false (enode< #() 'x))) + ;; symbols v.s. numbers + (progn + (true (enode< 0 'x)) + (false (enode< 'x 0))) + ;; vectors v.s. numbers + (progn + (true (enode< 0 #())) + (false (enode< #() 0))) + ;; numbers v.s. numbers + (progn + (true (enode< 0 1)) + (false (enode< 0 0)))) + +(defun sort-enodes-dump (enodes-dump) + (sort enodes-dump #'enode< :key #'second)) + +(defun dump-enodes (egraph) + "Dump EGRAPH's enodes as a normalized list for inspection and +comparison." + (sort-enodes-dump + (loop + :for enode :being :the :hash-key :of (enode-eclasses egraph) + :using (hash-value eclass-id) + :collect (list :enode (if (vectorp enode) + (copy-seq enode) + enode) + :eclass-id eclass-id)))) + +(defun dump-eclass (egraph eclass &aux (eclass-id (id eclass))) + "Dump EGRAPH's ECLASS as a list for inspection and comparison." + `(:eclass-id ,eclass-id + :enodes ,(copy-seq (enodes eclass)) + ,@(if (plusp (hash-table-count (parents eclass))) + (list :parents (sort (alexandria:hash-table-values (parents eclass)) + #'enode< + #++ #'(lambda (a b) + (when (and (numberp a))) <))) + (list :root)) + ,@(let ((canonical-id (eclass-find egraph eclass-id))) + (unless (= eclass-id canonical-id) + (list := canonical-id))))) + +(defun sort-eclasses-dump (eclasses-dump) + (sort eclasses-dump #'< :key #'second)) + +(defun dump-eclasses (egraph) + "Dump EGRAPH's eclasses as a list for inspection and comparison." + (sort-eclasses-dump + (loop + :for eclass-id :being :the :hash-key :of (eclasses egraph) + :using (hash-value eclass) + :collect (dump-eclass egraph eclass)))) + +(defun dump-egraph (egraph) + "Dump EGRPAH as a list for inspection and comparison." + `(,@(when (plusp (hash-table-count (eclasses egraph))) + (list :enodes (dump-enodes egraph))) + ,@(when (plusp (hash-table-count (eclasses egraph))) + (list :eclasses (dump-eclasses egraph))) + ,@(when (pending egraph) + (list :pending (pending egraph))))) + +(defun normalize-egraph-dump (egraph-dump) + (setf #1=(getf egraph-dump :enodes) (sort-enodes-dump #1#) + #2=(getf egraph-dump :eclasses) (sort-eclasses-dump #2#) + #| TODO maybe normalize "pending" |#)) + +(defun egraph-dumps-equal-p (egraph-dump1 egraph-dump2) + (let ((egraph-dump1 (normalize-egraph-dump (copy-seq egraph-dump1))) + (egraph-dump2 (normalize-egraph-dump (copy-seq egraph-dump2)))) + (equalp egraph-dump1 egraph-dump2))) + + + +(define-test+run "add enode(s) - snapshot tests" + (let* ((egraph (make-egraph))) + (is egraph-dumps-equal-p + '() + (dump-egraph egraph))) + (let* ((egraph (make-egraph))) + (egraph-add-enode egraph 'x) + (is egraph-dumps-equal-p + '(:enodes ((:enode x :eclass-id 0)) + :eclasses ((:eclass-id 0 :enodes #(x) :root))) + (dump-egraph egraph))) + ;; Here, we add the e-node 'x twice + (let ((egraph (make-egraph))) + (egraph-add-enode egraph 'x) + (egraph-add-enode egraph 'x) + (is egraph-dumps-equal-p + '(:enodes ((:enode x :eclass-id 0)) + :eclasses ((:eclass-id 0 :enodes #(x) :root))) + (dump-egraph egraph))) + ;; Here, we add the same _FORM_ twice + (let* ((egraph (make-egraph))) + (add-form egraph '(+ 1 2)) + (add-form egraph '(+ 1 2)) + (is egraph-dumps-equal-p + '(:enodes ((:enode 1 :eclass-id 0) + (:enode 2 :eclass-id 1) + (:enode #(+ 0 1) :eclass-id 2)) + :eclasses ((:eclass-id 0 :enodes #(1) :parents (2)) + (:eclass-id 1 :enodes #(2) :parents (2)) + (:eclass-id 2 :enodes #(#(+ 0 1)) :root))) + (dump-egraph egraph))) + (let ((egraph (make-egraph))) + (add-form egraph '(+ x y)) + (add-form egraph '(+ x 2)) + (add-form egraph '(+ y y)) + (is egraph-dumps-equal-p + '(:enodes ((:enode 2 :eclass-id 3) + (:enode x :eclass-id 0) + (:enode y :eclass-id 1) + (:enode #(+ 0 1) :eclass-id 2) + (:enode #(+ 0 3) :eclass-id 4) + (:enode #(+ 1 1) :eclass-id 5)) + :eclasses ((:eclass-id 0 :enodes #(x) :parents (2 4)) + (:eclass-id 1 :enodes #(y) :parents (2 5)) + (:eclass-id 2 :enodes #(#(+ 0 1)) :root) + (:eclass-id 3 :enodes #(2) :parents (4)) + (:eclass-id 4 :enodes #(#(+ 0 3)) :root) + (:eclass-id 5 :enodes #(#(+ 1 1)) :root))) + (dump-egraph egraph))) + (let ((egraph (make-egraph))) + (add-form egraph '(/ (* a 2) 2)) + (is egraph-dumps-equal-p + '(:enodes ((:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 0 1) :eclass-id 2) + (:enode #(/ 2 1) :eclass-id 3)) + :eclasses ((:eclass-id 0 :enodes #(a) :parents (2)) + (:eclass-id 1 :enodes #(2) :parents (2 3)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (3)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :root))) + (dump-egraph egraph)))) + +(define-test+run "add enode(s) - snapshot tests - step by step - (+ x y)" + (let ((egraph (make-egraph))) + (macrolet ((check (when expected) + `(is egraph-dumps-equal-p ,expected (dump-egraph egraph) + ,when))) + (check "after initialization" '()) + (add-form egraph 'x) + (check "after adding the form 'x" + '(:enodes ((:enode x :eclass-id 0)) + :eclasses ((:eclass-id 0 :enodes #(x) :root)))) + (add-form egraph 'y) + (check "after adding the form 'y" + '(:enodes ((:enode x :eclass-id 0) + (:enode y :eclass-id 1)) + :eclasses ((:eclass-id 0 :enodes #(x) :root) + (:eclass-id 1 :enodes #(y) :root)))) + (add-form egraph '(+ x y)) + (check + "after adding the form '(+ x y)" + '(:enodes ((:enode x :eclass-id 0) + (:enode y :eclass-id 1) + (:enode #(+ 0 1) :eclass-id 2)) + :eclasses ((:eclass-id 0 :enodes #(x) :parents (2)) + (:eclass-id 1 :enodes #(y) :parents (2)) + (:eclass-id 2 :enodes #(#(+ 0 1)) :root))))))) + +(define-test+run "add enode(s) - snapshot tests - step by step - x is equivalent to y" + (let ((egraph (make-egraph))) + (macrolet ((check (when expected) + `(is egraph-dumps-equal-p ,expected (dump-egraph egraph) + ,when))) + (check "after initialization" '()) + (add-form egraph 'x) + (check "after adding the form 'x" + '(:enodes ((:enode x :eclass-id 0)) + :eclasses ((:eclass-id 0 :enodes #(x) :root)))) + (add-form egraph 'y) + (check "after adding the form 'y" + '(:enodes ((:enode x :eclass-id 0) + (:enode y :eclass-id 1)) + :eclasses ((:eclass-id 0 :enodes #(x) :root) + (:eclass-id 1 :enodes #(y) :root)))) + ;; TODO maybe add a convenience method "merge-forms" + (merge-eclass egraph + (eclass-id egraph 'x) + (eclass-id egraph 'y)) + (check "after merging the e-classes for the enodes 'x and 'y" + '(:enodes ((:enode x :eclass-id 0) + (:enode y :eclass-id 1)) + :eclasses ((:eclass-id 0 :enodes #(x) :root) + (:eclass-id 1 :enodes #(y) :root := 0)) + :pending (0))) + (rebuild egraph) + (check "after rebuild" + ;; TODO This is technically correct (AFAIU), but it would + ;; be nice to catch the cases where we merge eclasses + ;; that represents only 1 form. + '(:enodes ((:enode x :eclass-id 0) + (:enode y :eclass-id 1)) + :eclasses ((:eclass-id 0 :enodes #(x) :root) + (:eclass-id 1 :enodes #(y) :root := 0))))))) + +(define-test+run "add enode(s) - snapshot tests - 1 + 1 = 2" + (let ((egraph (make-egraph))) + (macrolet ((check (when expected) + `(is egraph-dumps-equal-p ,expected (dump-egraph egraph) + ,when))) + (merge-eclass egraph + (add-form egraph '2) + (prog1 (add-form egraph '(+ 1 1)) + (check "before merging the e-classes for the enodes '2 and '(+ 1 1)" + '(:enodes ((:enode 1 :eclass-id 1) + (:enode 2 :eclass-id 0) + (:enode #(+ 1 1) :eclass-id 2)) + :eclasses ((:eclass-id 0 :enodes #(2) :root) + (:eclass-id 1 :enodes #(1) :parents (2)) + (:eclass-id 2 :enodes #(#(+ 1 1)) :root)))))) + (check "after merging the e-classes for the enodes '2 and '(+ 1 1)" + '(:enodes ((:enode 1 :eclass-id 1) + (:enode 2 :eclass-id 0) + (:enode #(+ 1 1) :eclass-id 2)) + :eclasses ((:eclass-id 0 :enodes #(2) :root) + (:eclass-id 1 :enodes #(1) :parents (2)) + (:eclass-id 2 :enodes #(#(+ 1 1)) :root := 0)) + :pending (0))) + (rebuild egraph) + (check "after rebuild" + '(:enodes ((:enode 1 :eclass-id 1) + (:enode 2 :eclass-id 0) + (:enode #(+ 1 1) :eclass-id 2)) + :eclasses ((:eclass-id 0 :enodes #(2) :root) + (:eclass-id 1 :enodes #(1) :parents (2)) + (:eclass-id 2 :enodes #(#(+ 1 1)) :root := 0))))))) + + +;; TODO add 2; add (+ (+ 1 1) 1); assert 2 = (+ 1 1) then eclass for +;; the value "2" should have the same parent all the equivalent +;; classes. Perhaphs only keep track of the parents in the class +;; representative? +(define-test+run "add enode(s) - snapshot tests - 1 + 1 = 2 & 3 + (1 + 1)" + (let ((egraph (make-egraph))) + (macrolet ((check (when expected) + `(is egraph-dumps-equal-p ,expected (dump-egraph egraph) + ,when))) + (let* ((e2 (add-form egraph '2)) + (e1+1 (add-form egraph '(+ 1 1))) + (e3+ (add-form egraph '(+ 3 (+ 1 1))))) + (declare (ignorable e3+)) + (check "before merging the e-classes for the enodes '2 and '(+ 1 1)" + `(:enodes + ;; enodes for the value 1, 2 and 3 happen to have the + ;; eclass-id 1, 2 and 3. + #1=((:enode 1 :eclass-id 1) + (:enode 2 :eclass-id ,e2) + (:enode 3 :eclass-id 3) + (:enode #(+ 1 1) :eclass-id ,e1+1) + (:enode #(+ 3 2) :eclass-id ,e3+)) + :eclasses + ((:eclass-id 0 :enodes #(2) :root) + (:eclass-id 1 :enodes #(1) :parents (2)) + (:eclass-id 2 :enodes #(#(+ 1 1)) :parents (,e3+)) + (:eclass-id 3 :enodes #(3) :parents (,e3+)) + (:eclass-id 4 :enodes #(#(+ 3 2)) :root)))) + (merge-eclass egraph e2 e1+1) + (check "after merging the e-classes for the enodes '2 and '(+ 1 1)" + `(:enodes #1# + :eclasses + ((:eclass-id ,e2 :enodes #(2) :parents (,e3+)) + (:eclass-id 1 :enodes #(1) :parents (2)) + (:eclass-id ,e1+1 :enodes #(#(+ 1 1)) :parents (,e3+) := ,e2) + (:eclass-id 3 :enodes #(3) :parents (,e3+)) + (:eclass-id ,e3+ :enodes #(#(+ 3 2)) :root)) + :pending (0))) + (rebuild egraph) + (check "after rebuild" + `(:enodes #1# + :eclasses + ((:eclass-id ,e2 :enodes #(2) :parents (,e3+)) + (:eclass-id 1 :enodes #(1) :parents (2)) + (:eclass-id ,e1+1 :enodes #(#(+ 1 1)) :parents (,e3+) := ,e2) + (:eclass-id 3 :enodes #(3) :parents (,e3+)) + (:eclass-id ,e3+ :enodes #(#(+ 3 2)) :root)))))))) + +(define-test+run "add enode(s) - snapshot tests - a = a * 2 /2" + (let ((egraph (make-egraph))) + (macrolet ((check (when expected) + `(is egraph-dumps-equal-p ,expected (dump-egraph egraph) + ,when)) + (check-add (form expected) + `(progn + (add-form egraph ,form) + (check ,(format nil "after adding ~(~s~)" form) + ,expected))) + (check-merge (form1 form2 expected) + `(progn + (merge-eclass egraph + (add-form egraph ,form1) + (add-form egraph ,form2)) + (check ,(format nil "after merging ~(~s and ~s~)" + form1 form2) + ,expected)))) + (check-add + '(/ (* a 2) 2) + '(:enodes ((:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 0 1) :eclass-id 2) + (:enode #(/ 2 1) :eclass-id 3)) + :eclasses ((:eclass-id 0 :enodes #(a) :parents (2)) + (:eclass-id 1 :enodes #(2) :parents (2 3)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (3)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :root)))) + (check-merge + '(* a 2) + '(ash a 1) + '(:enodes ((:enode 1 :eclass-id 4) + (:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 0 1) :eclass-id 2) + (:enode #(ash 0 4) :eclass-id 5) + (:enode #(/ 2 1) :eclass-id 3)) + :eclasses ((:eclass-id 0 :enodes #(a) :parents (2 5)) + (:eclass-id 1 :enodes #(2) :parents (2 3)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (3)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :root) + (:eclass-id 4 :enodes #(1) :parents (5)) + (:eclass-id 5 :enodes #(#(ash 0 4)) :parents (3) := 2)) + :pending (2))) + (check-merge + '(/ (* a 2) 2) + '(* a (/ 2 2)) + '(:enodes ((:enode 1 :eclass-id 4) + (:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 0 1) :eclass-id 2) + (:enode #(ash 0 4) :eclass-id 5) + (:enode #(* 0 6) :eclass-id 7) + (:enode #(/ 1 1) :eclass-id 6) + (:enode #(/ 2 1) :eclass-id 3)) + :eclasses ((:eclass-id 0 :enodes #(a) :parents (2 5 7)) + (:eclass-id 1 :enodes #(2) :parents (2 3 6)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (3)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :root) + (:eclass-id 4 :enodes #(1) :parents (5)) + (:eclass-id 5 :enodes #(#(ash 0 4)) :parents (3) := 2) + (:eclass-id 6 :enodes #(#(/ 1 1)) :parents (7)) + (:eclass-id 7 :enodes #(#(* 0 6)) :root := 3)) + :pending (3 2))) + (check-merge + '(/ 2 2) + 1 + '(:enodes ((:enode 1 :eclass-id 4) + (:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 0 1) :eclass-id 2) + (:enode #(ash 0 4) :eclass-id 5) + (:enode #(* 0 6) :eclass-id 7) + (:enode #(/ 1 1) :eclass-id 6) + (:enode #(/ 2 1) :eclass-id 3)) + :eclasses ((:eclass-id 0 :enodes #(a) :parents (2 5 7)) + (:eclass-id 1 :enodes #(2) :parents (2 3 6)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (3)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :root) + (:eclass-id 4 :enodes #(1) :parents (5 7) := 6) + (:eclass-id 5 :enodes #(#(ash 0 4)) :parents (3) := 2) + (:eclass-id 6 :enodes #(#(/ 1 1)) :parents (5 7)) + (:eclass-id 7 :enodes #(#(* 0 6)) :root := 3)) + :pending (6 3 2))) + (rebuild egraph) + (check "after rebuild" + '(:enodes ((:enode 1 :eclass-id 4) + (:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 0 1) :eclass-id 2) + (:enode #(ash 0 4) :eclass-id 5) + (:enode #(* 0 6) :eclass-id 3) + (:enode #(/ 1 1) :eclass-id 6) + (:enode #(/ 2 1) :eclass-id 3)) + :eclasses ((:eclass-id 0 :enodes #(a) :parents (2 5 7)) + (:eclass-id 1 :enodes #(2) :parents (2 3 6)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (3)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :root) + (:eclass-id 4 :enodes #(1) :parents (5 7) := 6) + (:eclass-id 5 :enodes #(#(ash 0 4)) :parents (3) := 2) + (:eclass-id 6 :enodes #(#(/ 1 1)) :parents (2 3)) + (:eclass-id 7 :enodes #(#(* 0 6)) :root := 3))))))) + + +(define-test+run "can I extract something useful?" + (let ((egraph (make-egraph)) + (input '(/ (* a 2) 2))) + (labels ((add* (form) + (add-form egraph form)) + (merge* (form1 form2) + (merge-eclass egraph (add* form1) (add* form2))) + (dump-eclass* (eclass) + (dump-eclass egraph eclass))) + (add* input) + (merge* '(* a 2) + '(ash a 1)) + (merge* '(/ (* a 2) 2) + '(* a (/ 2 2))) + (merge* '(/ 2 2) + 1) + (merge* '(* a 1) + 'a) + (rebuild egraph) + (is egraph-dumps-equal-p + '(:enodes + ((:enode 1 :eclass-id 4) + (:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 8 1) :eclass-id 2) + (:enode #(* 8 6) :eclass-id 8) + (:enode #(/ 1 1) :eclass-id 6) + (:enode #(/ 2 1) :eclass-id 8) + (:enode #(ash 8 6) :eclass-id 2)) + :eclasses + ((:eclass-id 0 :enodes #(a) :parents (2 5 7 8) := 8) + (:eclass-id 1 :enodes #(2) :parents (2 3 6)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (8)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :parents (2 2 8) := 8) + (:eclass-id 4 :enodes #(1) :parents (5 7 8) := 6) + (:eclass-id 5 :enodes #(#(ash 0 4)) :parents (3) := 2) + (:eclass-id 6 :enodes #(#(/ 1 1)) :parents (2 8)) + (:eclass-id 7 :enodes #(#(* 0 6)) :root := 8) + (:eclass-id 8 :enodes #(#(* 0 4)) :parents (2 2 8)))) + ;; (add* input) = 3 + (dump-egraph egraph)) + ;; Finding the "root eclasses" + (is equalp + '((:eclass-id 7 :enodes #(#(* 0 6)) :root := 8)) + (loop + :for eclass-id :being + :the :hash-key :of (eclasses egraph) + :using (hash-value eclass) + :when (zerop (hash-table-count (parents eclass))) + :collect (dump-eclass egraph eclass)) + "when trying to find the roots") + ;; TODO The following next 2 tests were working because of a bug + ;; with how the parents when tracked. + #++ + (is equalp + '((:eclass-id 0 :enodes #(a) :parents (2 5 7 8) := 8) + (:eclass-id 3 :enodes #(#(/ 2 1))) + (:eclass-id 7 :enodes #(#(* 0 6)) := 3) + (:eclass-id 8 :enodes #(#(* 0 4)))) + (mapcar #'dump-eclass* (root-eclasses egraph)) + "when trying to find the roots and their closure") + #++ + ;; Victory! + (is equalp + #(a) + (smallest-enodes + (root-eclasses egraph)))))) + + + +;;; Work in Progress - ematching! + + +(defun make-egraph* (input &rest other-inputs) + (let ((egraph (make-egraph))) + (add-input egraph input) + (map nil (lambda (i) (add-form egraph i)) other-inputs) + (rebuild egraph) + egraph)) + +#++ +(progn + (defun test-simple-rewrite (input pattern template) + (test-simple-rewrite* input (make-rewrite pattern template))) + (defun test-simple-rewrite* (input rewrite) + (format t "~%~%") + (let ((egraph (if (typep input 'egraph) input (make-egraph* input)))) + (map-egraph #'print egraph :limit 100) + (format t "~%~%") + (let ((before (dump-egraph egraph)) + (after (progn (apply-rewrite egraph rewrite) + (rebuild egraph) + (dump-egraph egraph)))) + (progn + (format t "~%~%") + (format t "~&Applying the rewrite rule:~& ~s~& ~s" + (rewrite-pattern rewrite) + (rewrite-template rewrite)) + (format t "~%~%") + (format t "~&Enodes before:~%~{ ~s~^~%~}" (second before)) + (format t "~&Enodes after :~%~{ ~s~^~%~}" (second after)) + (format t "~%~%") + (format t "~&Eclasses before:~%~{ ~s~^~%~}" (fourth before)) + ;; (format t "~&Eclasses after :~%~{ ~s~^~%~}" (fourth after)) + (format t "~&Eclasses after:") + (dolist (eclass-ish (fourth after)) + (format t "~& ~s's forms:" eclass-ish) + (let ((eclass-id (second eclass-ish))) + (map-stream #'(lambda (form) + (format t "~& ~a" form)) + (stream-eclass egraph (eclass egraph eclass-id))))) + (format t "~%~%") + (loop + :for input-eclass-id :in (input-eclasses egraph) + :do + (format t "~&Forms in input e-class ~d:" input-eclass-id) + (map-stream + (lambda (eclass-id) + (map-stream #'(lambda (form) + (format t "~&-> ~a" form)) + (stream-eclass egraph (eclass egraph eclass-id)))) + (stream-equivalent-eclasses egraph input-eclass-id) + :limit 100) + ;; (map-egraph #'print egraph :limit 100) + )) + egraph))) + + #++ + (let ((egraph (test-simple-rewrite '(/ a a) '(/ ?x ?x) 1))) + (test-simple-rewrite egraph 1 '(/ ?x ?x))) + + #++ + ((untrace) + (trace + ;; :wherein test-simple-rewrite + pattern-substitute + breeze.egraph::match-rewrite + breeze.egraph::match-eclass + breeze.egraph::match-enode + ;; merge-eclass + add-form + breeze.egraph::egraph-add-enode + breeze.egraph::form-to-enode + breeze.egraph::sequence-to-enode + breeze.egraph::atom-to-enode + ;; match + ;; add-parent + )) + + ;; #++ + (let ((egraph (test-simple-rewrite '(+ a b c) '(+ ?x ?y ?z) '(+ ?x (+ ?y ?z))))) + (test-simple-rewrite egraph '(+ ?x ?y ?z) '(+ (+ ?x ?y) ?z)) + (test-simple-rewrite egraph '(+ ?x ?y) '(+ ?y ?x)) + (setf *e* egraph)) + + #++ + (let ((egraph (test-simple-rewrite '(* a 2) '(* ?x 2) '(ash ?x 1)))) + ;; (test-simple-rewrite egraph '(+ ?x ?y ?z) '(+ (+ ?x ?y) ?z)) + ;; (test-simple-rewrite egraph '(+ ?x ?y) '(+ ?y ?x)) + (setf *e* egraph)) + + #++ + (let ((egraph (test-simple-rewrite '(+ 1 (* a 2)) '(* ?x 2) '(ash ?x 1)))) + (test-simple-rewrite egraph '(* ?x 2) '(ash ?x 1)) + ;; (test-simple-rewrite egraph '(+ ?x ?y ?z) '(+ (+ ?x ?y) ?z)) + ;; (test-simple-rewrite egraph '(+ ?x ?y) '(+ ?y ?x)) + (setf *e* egraph)) + + #++ + (let ((egraph (test-simple-rewrite '(/ (* a 2) 2) '(/ (* ?x ?y) ?y) '?x))) + (setf *e* egraph)) + + ;; '(/ (* a 2) 2) + ;; (untrace) + ) + +#| +Input: +(+ a b c) + +Rewrites (applied in this order): +'(+ ?x ?y ?z) '(+ ?x (+ ?y ?z)) +'(+ ?x ?y ?z) '(+ (+ ?x ?y) ?z) +'(+ ?x ?y) '(+ ?y ?x) + +Forms represented by the egraph: +(+ A B C) +(+ A (+ B C)) +(+ (+ A B) C) +(+ (+ B C) A) +(+ C (+ A B)) +|# + + +(define-test+run "apply 1 rewrite" + (is egraph-dumps-equal-p + '(:enodes + ((:enode 2 :eclass-id 1) + (:enode a :eclass-id 0) + (:enode #(* 3 1) :eclass-id 2) + (:enode #(/ 2 1) :eclass-id 3)) + :eclasses + ((:eclass-id 0 :enodes #(a) :parents (2) := 3) + (:eclass-id 1 :enodes #(2) :parents (2 3)) + (:eclass-id 2 :enodes #(#(* 0 1)) :parents (3)) + (:eclass-id 3 :enodes #(#(/ 2 1)) :parents (2)))) + (let ((egraph (make-egraph* '(/ (* a 2) 2))) + (rewrite (make-rewrite '(/ (* ?x ?y) ?y) '?x))) + (apply-rewrite egraph rewrite) + (dump-egraph (rebuild egraph)) + #++ ;; TODO + (smallest-enodes + (root-eclasses egraph))))) + +#++ ;; TODO It would be nice to be able to add a form as a vector into +;; an egraph. I think it could help with performance, because +;; applying a rewrite and adding the resulting "substituted" form +;; would not involve conversion between lists and vectors anymore. +(let ((egraph (make-egraph))) + (add-form egraph #(/ #(* a 2) 2)) + (dump-egraph egraph)) + +#++ +(defparameter *e* nil) + + + +#++ +(let ((egraph (make-egraph* '(/ (* a 2) 2))) + (*print-readably* nil) + (*print-level* nil) + (*print-length* nil)) + (format t "~&=========================================") + (format t "~&=========================================") + (format t "~&=========================================") + (let ((rewrites + (list + ;; These are not all sounds + (make-rewrite '(/ (* ?x ?y) ?y) '?x) + (make-rewrite '(* (/ ?x ?y) ?y) '?x) + + (make-rewrite '(* ?x 2) '(ash ?x 1)) + + (make-rewrite '(/ ?x ?x) 1) + + (make-rewrite '(/ (* ?x ?y) ?z) '(* ?x (/ ?y ?z))) + + ;; (make-rewrite '(/ ?x 1) '?x) + ;; (make-rewrite '(* ?x 1) '?x) + ;; (make-rewrite '(* 1 ?x) '?x) + ;; (make-rewrite '(/ 0 ?x) 0) + ))) + (loop :repeat 1 + :do + (format t "~&=========================================") + (loop :for rewrite :in rewrites + :do (test-simple-rewrite* egraph rewrite) + #++ (progn (apply-rewrite egraph rewrite) + (rebuild egraph) + (map-egraph #'print egraph :limit 100))))) + egraph) + +;; + +#++ +(progn + (untrace) + (dump-egraph *e*) + (map-egraph #'print *e* :limit 100)) + + +;; (= 0 ?x) => (zerop x) +;; (= x ?0) => (zerop x) +;; (and (zerop ?x (= ?x ?y))) => (= 0 ?x ?y) diff --git a/tests/listener.lisp b/tests/listener.lisp new file mode 100644 index 00000000..9af91acf --- /dev/null +++ b/tests/listener.lisp @@ -0,0 +1,7 @@ + + +;; TODO eval-last-expression +'asdf +'|asdf| +'|CL|::|IN-PACKAGE| +;; ^^^ Slime doesn't handle this one correctly diff --git a/tests/lossless-reader.lisp b/tests/lossless-reader.lisp index b9ec739b..169ee21b 100644 --- a/tests/lossless-reader.lisp +++ b/tests/lossless-reader.lisp @@ -1,69 +1,6 @@ (cl:in-package #:cl-user) -(defpackage #:breeze.test.lossless-reader - (:documentation "Test package for #:breeze.lossless-reader") - (:use #:cl #:breeze.lossless-reader) - (:import-from #:breeze.lossless-reader - ;; state - #:state - #:source - #:pos - #:tree - #:make-state - ;; nodes - #:+end+ - #:node - #:valid-node-p - ;; node constructors - #:block-comment - #:parens - #:punctuation - #:token - #:whitespace - #:line-comment - ;; Symbols used in the returns - #:quote ; this ones from cl actually - #:quasiquote - #:dot - #:at - #:comma - #:sharp - ;; state utilities - #:at - #:donep - #:valid-position-p - #:*state-control-string* - #:state-context - ;; parsing utilities - #:read-char* - #:find-all - #:not-terminatingp - #:read-string* - #:read-while - ;; sub parser - #:read-line-comment - #:read-parens - #:read-punctuation - #:read-quoted-string - #:read-string - #:read-token - #:read-whitespaces - #:read-block-comment - ;; top-level parsing/unparsing - #:parse - #:parse* - #:unparse) - (:import-from #:parachute - #:define-test - #:define-test+run - #:is - #:true - #:false - #:of-type) - (:import-from #:breeze.kite - #:is-equalp)) - (in-package #:breeze.test.lossless-reader) #| @@ -96,12 +33,6 @@ newline or +end+) ;;; testing helpers -(defvar *test-strings* (make-hash-table :test 'equal)) - -(defun register-test-string (string) - (setf (gethash string *test-strings*) t) - string) - (defmacro with-state ((string &optional more-labels) &body body) (alexandria:once-only (string) `(let ((state (make-state (register-test-string ,string)))) @@ -109,9 +40,12 @@ newline or +end+) ;; remainder: the input is only used (unless (equalp got expected)) ;; the input is used to give (labels ((test* (got &optional expected) - (is-equalp ,string got expected - *state-control-string* - (state-context state))) + (is-equalp + :input ,string + :got got + :expected expected + :description *state-control-string* + :format-args (state-context state))) ,@more-labels) (declare (ignorable (function test*))) ,@ (loop :for (label . _) :in more-labels @@ -140,6 +74,22 @@ newline or +end+) ,@more-labels) ,@body)) +#++ +(with-state ("asdf") + (test* t t)) + + + +#++ +(with-state ("asdf") + (format nil "This is a bug: read-any returned an invalid node, but we're not done reading the file...~%~?" + *state-control-string* + (state-context state))) + +#++ +(with-state ("asdf") + (state-context state)) + ;;; Reader position (in the source string) @@ -167,25 +117,37 @@ newline or +end+) ("" (test* (at state -1) nil) (test* (at state 0) nil) - (test* (at state 1) nil) - (test* (at state -1 #\a) nil) - (test* (at state 0 #\b) nil) - (test* (at state 1 #\c) nil)) + (test* (at state 1) nil)) ("c" (test* (at state -1) nil) (test* (at state 0) #\c) - (test* (at state 1) nil) - (test* (at state -1 #\c) nil) - (test* (at state 0 #\c) #\c) - (test* (at state 0 #\a) nil) - (test* (at state 1 #\c) nil)))) + (test* (at state 1) nil)))) + +(define-test+run at= + :depends-on (at) + (with-state* () + ("" + (test* (at= state -1 #\a) nil) + (test* (at= state 0 #\b) nil) + (test* (at= state 1 #\c) nil)) + ("c" + (test* (at= state -1 #\c) nil) + (test* (at= state 0 #\c) #\c) + (test* (at= state 0 #\a) nil) + (test* (at= state 1 #\c) nil)))) ;; TODO test "current-char" (define-test+run current-char) +;; TODO test "current-char=" +(define-test+run current-char=) + ;; TODO test "next-char" (define-test+run next-char) +;; TODO test "next-char=" +(define-test+run next-char=) + ;;; Low-level parsing helpers @@ -233,7 +195,7 @@ newline or +end+) (defun test-find-all (needle string expected) (register-test-string string) (register-test-string needle) - (is-equalp + (is-equalp* (list 'find-all needle string) (find-all needle string) expected)) @@ -263,9 +225,12 @@ newline or +end+) (defun test-read-block-comment (input expected-end) (with-state (input) - (is-equalp input (read-block-comment state) - (when expected-end - (block-comment 0 expected-end))))) + (is-equalp + :input input + :got (read-block-comment state) + :form `(read-block-comment ,state) + :expected (when expected-end + (block-comment 0 expected-end))))) (define-test+run read-block-comment :depends-on (read-string*) @@ -289,17 +254,419 @@ newline or +end+) (when expected-end (line-comment 0 expected-end))))) -(define-test+run read-line-comment +(define-test read-line-comment (test-read-line-comment "" nil) - (test-read-line-comment ";" +end+) - (test-read-line-comment "; asdf~%" 7)) + (test-read-line-comment ";" 1) + (test-read-line-comment "; asdf~%" 6)) + + + +(defparameter *sharpsign-reader-test-cases* (make-hash-table :test 'equal)) + +(defun test-read-sharpsign* (&key + sharpsing-reader-function + node-type + input + expected-end + expected-pos + expected-children + given-numeric-argument) + "Helps testing the read-sharpsign-* functions." + (let* ((starting-position (if (listp input) (length (first input)) 1)) + (input (if (listp input) (apply 'concatenate 'string input) input)) + (expected-end (or expected-end (length input))) + (expected-pos (or expected-pos expected-end))) + (with-state (input) + (setf (pos state) starting-position) + (let* ((expected (node node-type 0 + expected-end + expected-children)) + (got + (is-equalp + :input input + :got (funcall sharpsing-reader-function + state + ;; Assumes we started reading the + ;; # as the first character. + 0 + given-numeric-argument) + :form (list sharpsing-reader-function + state 0 given-numeric-argument) + :expected expected))) + (setf (gethash input *sharpsign-reader-test-cases*) expected) + (when (and got (plusp expected-end)) + (is-equalp + :input input + :expected expected-pos + :form `(pos ,state) + :got (pos state) + :description " the state's position after reading is wrong:")) + got)))) + + +;;; #\ + +(defun test-read-sharpsign-backslash (input expected-end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-backslash + :node-type 'sharp-char + :input input + :expected-end expected-end + :expected-children (unless (= +end+ expected-end) + (token 1 expected-end)))) + +(define-test+run read-sharpsign-backslash + (test-read-sharpsign-backslash "#\\" +end+) + (test-read-sharpsign-backslash "#\\ " 3) + (test-read-sharpsign-backslash "#\\ " 3) + (test-read-sharpsign-backslash "#\\Space" 7) + (test-read-sharpsign-backslash "#\\Space " 7) + (test-read-sharpsign-backslash "#\\ Space" 8) + (test-read-sharpsign-backslash "#\\bell" 6) + (test-read-sharpsign-backslash "#\\;" 3)) + + + +;;; #' + +(defun test-read-sharpsign-quote (input child expected-end) + (test-read-sharpsign* + :sharpsing-reader-function #'read-sharpsign-quote + :node-type 'sharp-function + :input input + :expected-end expected-end + :expected-children child)) + +(define-test+run read-sharpsign-quote + (test-read-sharpsign-quote "#'" nil +end+) + (test-read-sharpsign-quote "#' " (list (whitespace 2 3)) +end+) + (test-read-sharpsign-quote "#'a" (list (token 2 3)) 3) + (test-read-sharpsign-quote "#' a" (list (whitespace 2 3) + (token 3 4)) + 4) + (test-read-sharpsign-quote "#'(lambda...)" (list (parens 2 13 + (list (token 3 12)))) + 13)) + + +;;; #( + +(defun test-read-sharpsign-left-parens (input child expected-end) + (test-read-sharpsign* + :sharpsing-reader-function #'read-sharpsign-left-parens + :node-type 'sharp-vector + :input input + :expected-end expected-end + :expected-children child)) + +(define-test+run read-sharpsign-left-parens + (test-read-sharpsign-left-parens "#()" (parens 1 3) 3) + (test-read-sharpsign-left-parens "#( )" (parens 1 4 (whitespace 2 3)) 4) + (test-read-sharpsign-left-parens '("#1" "()") (parens 2 4) 4) + (test-read-sharpsign-left-parens '("#2" "( )") (parens 2 5 (whitespace 3 4)) 5)) + + +;;; #* + +(defun test-read-sharpsign-asterisk (input &key child end n) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-asterisk + :node-type 'sharp-bitvector + :input input + :expected-end end + :expected-children child + :given-numeric-argument n)) + +(define-test+run read-sharpsign-asterisk + (test-read-sharpsign-asterisk '("#" "*")) + (test-read-sharpsign-asterisk '("#" "* ") :end 2) + (test-read-sharpsign-asterisk '("#" "*0") :child 0) + (test-read-sharpsign-asterisk '("#0" "*") :n 0) + (test-read-sharpsign-asterisk '("#2" "*0") :child 0) + (test-read-sharpsign-asterisk '("#2" "*0") :n 2 :child 0) + ;; TODO this is actually a syntax error, as "101" is longer than 2 + (test-read-sharpsign-asterisk '("#2" "*101") :child 5 :n 2)) + + +;;; #: + +(defun test-read-sharpsign-colon (input child &optional expected-end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-colon + :node-type 'sharp-uninterned + :input input + :expected-end expected-end + :expected-children child)) + + +(define-test+run read-sharpsign-colon + (test-read-sharpsign-colon "#:" (token 2 2) 2) + (test-read-sharpsign-colon "#: " (token 2 2) 2) + (test-read-sharpsign-colon "#:||" (token 2 4) 4) + (test-read-sharpsign-colon "#:|| " (token 2 4) 4) + (test-read-sharpsign-colon "#: a" (token 2 2) 2) + (test-read-sharpsign-colon "#: a " (token 2 2) 2) + (test-read-sharpsign-colon "#:asdf" (token 2 6))) + +#++ +(progn + (read-from-string "#:") + (read-from-string "#: ") + (read-from-string "#: a") + ;; they all return => #:|| + ) + + +;;; #. + +(defun test-read-sharpsign-dot (input child expected-end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-dot + :node-type 'sharp-eval + :input input + :expected-end expected-end + :expected-children child)) + +(define-test+run read-sharpsign-dot + (test-read-sharpsign-dot "#." nil +end+) + (test-read-sharpsign-dot "#.a" (list (token 2 3)) 3) + (test-read-sharpsign-dot "#. a" (list (whitespace 2 3) + (token 3 4)) + 4)) + + +;;; #c + +(defun test-read-sharpsign-c (input &key child end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-c + :node-type 'sharp-complex + :input input + :expected-end end + :expected-children child)) + +(define-test+run read-sharpsign-c + (test-read-sharpsign-c "#c" :end +end+) + (test-read-sharpsign-c "#C" :end +end+) + (test-read-sharpsign-c "#cx" :end +end+) + (test-read-sharpsign-c "#Cx" :end +end+) + (test-read-sharpsign-c "#c1" :end +end+) + (test-read-sharpsign-c "#C1" :end +end+) + ;; N.B. #c(1) is actually invalid + (test-read-sharpsign-c "#c(1)" + :child (node 'parens 2 5 (list (node 'token 3 4)))) + (test-read-sharpsign-c "#C(1)" + :child (node 'parens 2 5 (list (node 'token 3 4)))) + (test-read-sharpsign-c "#c(1 2) a" + :child (node 'parens 2 7 + (list (node 'token 3 4) + (node 'whitespace 4 5) + (node 'token 5 6))) + :end 7) + (test-read-sharpsign-c "#C(1 2) a" + :child (node 'parens 2 7 + (list (node 'token 3 4) + (node 'whitespace 4 5) + (node 'token 5 6))) + :end 7)) + + +;;; #a + +(defun test-read-sharpsign-a (input &key child end n) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-a + :node-type 'sharp-array + :input input + :expected-end end + :expected-children child + :given-numeric-argument n)) + +(define-test+run read-sharpsign-a + (test-read-sharpsign-a '("#" "a") :end +end+) + (test-read-sharpsign-a '("#" "a ") :end +end+) + (test-read-sharpsign-a '("#" "a0") :end +end+) + (test-read-sharpsign-a '("#0" "a") :end +end+) + (test-read-sharpsign-a '("#2" "a0") :end +end+) + (test-read-sharpsign-a '("#2" "a0") :end +end+) + ;; TODO this is actually a syntax error, as "101" is longer than 2 + (test-read-sharpsign-a '("#2" "a()") :child (parens 3 5)) + (test-read-sharpsign-a '("#2" "a(1 2)") + :child (parens 3 8 + (list (token 4 5) + (whitespace 5 6) + (token 6 7)))) + (test-read-sharpsign-a '("#2" "A()") :child (parens 3 5))) + + +;;; #s + +(defun test-read-sharpsign-s (input &key child end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-s + :node-type 'sharp-structure + :input input + :expected-end end + :expected-children child)) + +(define-test+run read-sharpsign-s + (test-read-sharpsign-s "#s" :end +end+) + (test-read-sharpsign-s "#S" :end +end+) + (test-read-sharpsign-s "#S(node)" + :child (list (parens 2 8 (list (token 3 7))))) + (test-read-sharpsign-s "#S(node) foo" + :child (list (parens 2 8 (list (token 3 7)))) + :end 8)) + + +;;; #p + +(defun test-read-sharpsign-p (input &key child end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-p + :node-type 'sharp-pathname + :input input + :expected-end end + :expected-children child)) + +(define-test+run read-sharpsign-p + (test-read-sharpsign-p "#p" :end +end+) + (test-read-sharpsign-p "#P" :end +end+) + (test-read-sharpsign-p "#p\"/root/\"" + :child (list (node 'string 2 10)) + :end 10) + (test-read-sharpsign-p "#p\"/root/\" foo" + :child (list (node 'string 2 10)) + :end 10)) + + +;;; #=n + +(defun test-read-sharpsign-equal (input &key child end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-equal + :node-type 'sharp-label + :input input + :expected-end end + :expected-children child + :given-numeric-argument (getf child :label))) + +(define-test+run read-sharpsign-equal + (test-read-sharpsign-equal "#=" :end +end+) + (test-read-sharpsign-equal + '("#1" "=") + :child (list :label 1) + :end +end+) + (test-read-sharpsign-equal + '("#2" "= ") + :child (list :label 2 + :form (list (whitespace 3 4))) + :end +end+) + (test-read-sharpsign-equal + '("#3" "=(foo)") + :child (list :label 3 + :form (list (parens 3 8 (token 4 7)))))) + + + +;;; #n# + +(defun test-read-sharpsign-sharpsign (input &key child end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-sharpsign + :node-type 'sharp-reference + :input input + :expected-end end + :expected-children child + :given-numeric-argument child)) + +(define-test+run read-sharpsign-sharpsign + (test-read-sharpsign-sharpsign "##" :end +end+) + (test-read-sharpsign-sharpsign '("#1" "#") :child 1) + (test-read-sharpsign-sharpsign '("#2" "# ") :child 2 :end 3)) + + +;;; #+ + +(defun test-read-sharpsign-plus (input &key child end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-plus + :node-type 'sharp-feature + :input input + :expected-end end + :expected-children child)) + +(define-test+run read-sharpsign-plus + (test-read-sharpsign-plus "#+" :end +end+) + (test-read-sharpsign-plus "#++" :child (list (token 2 3))) + (test-read-sharpsign-plus + "#+ #+ x" + :child (list (whitespace 2 3) + (sharp-feature 3 7 + (list (whitespace 5 6) + (token 6 7)))))) + + +;;; #- + +(defun test-read-sharpsign-minus (input &key child end) + (test-read-sharpsign* + :sharpsing-reader-function 'read-sharpsign-minus + :node-type 'sharp-feature-not + :input input + :expected-end end + :expected-children child)) + +(define-test+run read-sharpsign-minus + (test-read-sharpsign-minus "#-" :end +end+) + (test-read-sharpsign-minus "#--" :child (list (token 2 3))) + (test-read-sharpsign-minus + "#- #- x" + :child (list (whitespace 2 3) + (sharp-feature-not 3 7 + (list (whitespace 5 6) + (token 6 7)))))) + + + +(defun test-read-sharpsign (input expected-type expected-end + &optional (expected-pos expected-end)) + (with-state (input) + (let ((got (is-equalp* input + (read-sharpsign-dispatching-reader-macro state) + (node expected-type 0 expected-end)))) + (when got + (is-equalp* input + expected-pos + (pos state)))))) + +(define-test+run read-sharpsign-dispatching-reader-macro + (loop :for input :being + :the :hash-key :of *sharpsign-reader-test-cases* + :using (hash-value expected) + :do (with-state (input) + (is-equalp + :input input + :got (read-sharpsign-dispatching-reader-macro state) + :expected expected + :form `(read-sharpsign-dispatching-reader-macro ,state) + ;; :description description + ;; :format-args format-args + )))) + + +;; (read-from-string "#\\ ") == (read-from-string "#\\Space") +;; This is an error (there must be no space between "#s" and "("): (read-from-string "#s ()") + + + (defun test-read-punctuation (input expected-type) (with-state (input) - (is-equalp input - (read-punctuation state) - (when expected-type - (punctuation expected-type 0))))) + (is-equalp* input + (read-punctuation state) + (when expected-type + (punctuation expected-type 0))))) (define-test+run read-punctuation :depends-on (current-char) @@ -342,7 +709,8 @@ newline or +end+) (test-read-string "" nil) (test-read-string "\"" +end+) (test-read-string "\"\"" 2) - (test-read-string "\" \"" 3)) + (test-read-string "\" \"" 3) + (test-read-string "\"~s\"" 4)) (define-test+run not-terminatingp (mapcar #'(lambda (char) @@ -350,13 +718,68 @@ newline or +end+) "~c is supposed to be a terminating character." char)) '(#\; #\" #\' #\( #\) #\, #\`))) + +(defun tsn (string &optional (start 0) (end (length string))) + (%token-symbol-node string start end)) + +(defun tsn-padded (string) + (let* ((prefix ": ") + (suffix " ") + (l (length string)) + (p (length prefix))) + (tsn (concatenate 'string prefix string suffix) + p (+ p l)))) + +(define-test token-symbol-node + (progn + (is equalp (node 'current-package-symbol 0 1) (tsn "x")) + (is equalp (node 'keyword 1 2) (tsn ":x")) + (is equalp (node 'uninterned-symbol 2 3) (tsn "#:x")) + (is equalp + (node 'qualified-symbol 0 3 + (list (node 'package-name 0 1) + (node 'symbol-name 2 3))) + (tsn "p:x")) + (is equalp + (node 'possibly-internal-symbol 0 4 + (list + (node 'package-name 0 1) + (node 'symbol-name 3 4))) + (tsn "p::x")) + (false (tsn "")) + (false (tsn "#:")) + (false (tsn "::")) + (false (tsn "p:::x")) + (false (tsn "p::")) + (false (tsn "::x")) + (false (tsn "a:a:x"))) + (progn + (is equalp (node 'current-package-symbol 3 4) (tsn-padded "x")) + (is equalp (node 'keyword 4 5) (tsn-padded ":x")) + (is equalp (node 'uninterned-symbol 5 6) (tsn-padded "#:x")) + (is equalp (node 'qualified-symbol 3 6 + (list (node 'package-name 3 4) + (node 'symbol-name 5 6))) + (tsn-padded "p:x")) + (is equalp (node 'possibly-internal-symbol 3 7 + (list (node 'package-name 3 4) + (node 'symbol-name 6 7))) + (tsn-padded "p::x")) + (false (tsn-padded "")) + (false (tsn-padded "#:")) + (false (tsn-padded "::")) + (false (tsn-padded "p:::x")) + (false (tsn-padded "p::")) + (false (tsn-padded "::x")) + (false (tsn-padded "a:a:x")))) + + (defun test-read-token (input expected-end) (with-state (input) (test* (read-token state) (when expected-end (token 0 expected-end))))) -;; TODO Fix read-token (define-test+run read-token :depends-on (current-char not-terminatingp @@ -374,7 +797,11 @@ newline or +end+) (test-read-token "arg| asdf |more|" +end+) (test-read-token "arg| asdf |more|mmoooore|done" 29) (test-read-token "arg| asdf |no |mmoooore|done" 13) - (test-read-token "look|another\\| case\\| didn't think of| " 38)) + (test-read-token "look|another\\| case\\| didn't think of| " 38) + (test-read-token "this.is.normal..." 17) + (test-read-token "\\asdf" 5) + (test-read-token "\\;" 2) + (test-read-token "a\\;" 3)) ;; TODO read-extraneous-closing-parens @@ -400,18 +827,16 @@ newline or +end+) ;; TODO read-any (define-test read-any) - ;;; Putting it all toghether -;; TODO parse (defun test-parse (input &rest expected) (register-test-string input) (let* ((state (parse input)) (tree (tree state))) (if expected - (is-equalp input tree expected) - (is-equalp input tree)))) + (is-equalp* input tree expected) + (is-equalp* input tree)))) (define-test+run "parse" :depends-on (read-parens) @@ -429,84 +854,94 @@ newline or +end+) (test-parse "#| #||# |#" (block-comment 0 10)) (test-parse "'" (punctuation 'quote 0)) (test-parse "`" (punctuation 'quasiquote 0)) - (test-parse "#" (punctuation 'sharp 0)) + ;; (test-parse "#" (punctuation 'sharp 0)) (test-parse "," (punctuation 'comma 0)) (test-parse "+-*/" (token 0 4)) (test-parse "123" (token 0 3)) - (test-parse "asdf#" (token 0 5)) + ;; (test-parse "asdf#" (token 0 5)) (test-parse "| asdf |" (token 0 8)) (test-parse "arg| asdf | " (token 0 11) (whitespace 11 12)) (test-parse "arg| asdf |more" (token 0 15)) (test-parse "arg| asdf |more|" (token 0 +end+)) (test-parse "arg| asdf " (token 0 +end+)) - (test-parse ";" (line-comment 0 +end+)) + (test-parse ";" (line-comment 0 1)) + (test-parse "; " (line-comment 0 2)) + (test-parse (format nil ";~%") (line-comment 0 1) (whitespace 1 2)) + (test-parse (format nil ";~%;") (line-comment 0 1) (whitespace 1 2) (line-comment 2 3)) (test-parse "(12" (parens 0 +end+ (token 1 3))) - (test-parse "\"" (node 'string 0 +end+))) - -(defun test-parse* (input &rest expected) - (register-test-string input) - (if expected - (is-equalp input (parse* input) expected) - (is-equalp input (parse* input)))) - -#++ -(define-test+run "parse*" - :depends-on (read-parens) - (eq (parse "") nil) - (test-parse* " " (whitespace 0 2)) - (test-parse* "#|" (block-comment 0 +end+)) - (test-parse* " #| " - (whitespace 0 1) - (block-comment 1 +end+) - #++ - (whitespace 3 4)) - (test-parse* "#||#" (block-comment 0 4)) - (test-parse* "#|#||#" (block-comment 0 +end+)) - (test-parse* "#| #||# |#" (block-comment 0 10)) - (test-parse* "'" (punctuation 'quote 0)) - (test-parse* "`" (punctuation 'quasiquote 0)) - (test-parse* "#" (punctuation 'sharp 0)) - (test-parse* "," (punctuation 'comma 0)) - (test-parse* "+-*/" (token 0 4)) - (test-parse* "123" (token 0 3)) - (test-parse* "asdf#" (token 0 5)) - (test-parse* "| asdf |" (token 0 8)) - (test-parse* "arg| asdf | " (token 0 11) (whitespace 11 12)) - (test-parse* "arg| asdf |more" (token 0 15)) - (test-parse* "arg| asdf |more|" (token 0 +end+)) - (test-parse* ";" (line-comment 0 +end+)) - (test-parse* "(12" (parens 0 +end+ (token 1 3))) - (test-parse* "\"" (node 'string 0 +end+))) - - -#| - - -(list - (parse "#<>") - (parse "#+")) - -http://www.lispworks.com/documentation/HyperSpec/Body/02_dh.htm - -(list of reader macros - "\\'(*:boxrcasp=+-<") - - -#) and # are **invalid** - -|# - -#++ -(multiple-value-bind (tree state) - (parse "(foo)") - (format nil "(~a ~a)" - "ignore-errors" - (node-content state (car tree)))) - + (test-parse "\"" (node 'string 0 +end+)) + (test-parse "\"\"" (node 'string 0 2)) + (test-parse "#:asdf" + (node 'sharp-uninterned 0 6 + (node 'token 2 6))) + (test-parse "#2()" + (node 'sharp-vector 0 4 + (node 'parens 2 4))) + (test-parse "#<>" (node 'sharp-unknown 0 +end+)) + (test-parse "#+ x" (node 'sharp-feature 0 4 + (list + (whitespace 2 3) + (token 3 4)))) + (test-parse "(char= #\\; c)" + (parens 0 13 + (list (token 1 6) + (whitespace 6 7) + (sharp-char 7 10 (token 8 10)) + (whitespace 10 11) + (token 11 12)))) + (test-parse "(#\\;)" (parens 0 5 + (list (sharp-char 1 4 (token 2 4))))) + (test-parse "#\\; " (sharp-char 0 3 (token 1 3)) (whitespace 3 4)) + (test-parse "`( asdf)" (node 'quasiquote 0 1) + (parens 1 8 + (list + (whitespace 2 3) + (token 3 7)))) + (test-parse "#\\Linefeed" (sharp-char 0 10 (token 1 10))) + (test-parse "#\\: asd" (sharp-char 0 3 (token 1 3)) (whitespace 3 4) (token 4 7)) + (test-parse "((( )))" (parens 0 8 (list (parens 1 7 (list (parens 2 6 (list (whitespace 3 5)))))))) + (test-parse "(#" (parens 0 +end+ (sharp-unknown 1 +end+))) + (test-parse "(#)" (parens 0 +end+ (sharp-unknown 1 +end+))) + (test-parse "(#) " + (parens 0 +end+ (sharp-unknown 1 +end+)) + #++ (whitespace 3 4)) + (test-parse "(#') " + (parens + 0 +end+ + (sharp-function + 1 +end+ + (list (node ':extraneous-closing-parens 3 +end+))))) + (test-parse "#1=#1#" + (sharp-label 0 6 + (list :label 1 :form + (list (sharp-reference 3 6 1))))) + (test-parse "(;)" (parens 0 -1 (list (line-comment 1 3)))) + ;; TODO This is wrong + (test-parse "#+;;" (sharp-feature 0 4 (list (line-comment 2 4)))) + ;; TODO Is that what I want? + (test-parse "#++;;" (sharp-feature 0 3 (list (token 2 3))) (line-comment 3 5)) + ;; TODO This is wrong... but _OMG_ + (test-parse (format nil "cl-user::; wtf~%reaally?") + (token 0 9) (line-comment 9 14) (whitespace 14 15) (token 15 23)) + ;; TODO This is silly + (test-parse ",@" (node 'comma 0 1) (node 'at 1 2)) + ;; TODO This is silly + (test-parse ",." (node 'comma 0 1) (node 'dot 1 2))) + +#++ ;; this is cursed +(read-from-string "cl-user::; wtf +reaally?") + +#++ ;; this is an error +(read-from-string "cl-user:; wtf +:reaally?") ;; Slightly cursed syntax: ;; "#+#." +;; e.g. "#+ #.(cl:quote x) 2" == "#+ x 2" +#++ +(read-from-string ":\|") ;;; Unparse @@ -516,7 +951,7 @@ http://www.lispworks.com/documentation/HyperSpec/Body/02_dh.htm (let* ((state (parse string)) (result (unparse state nil)) (success (equalp string result))) - (is-equalp (or context string) result string) + (is-equalp* (or context string) result string) (when (and success check-for-error) ;; Would be nice to (signal ...), not error, just signal, when ;; there's a parsing failure, because right now it's pretty hard @@ -531,20 +966,45 @@ http://www.lispworks.com/documentation/HyperSpec/Body/02_dh.htm (state-context state)))))) success)) -(progn - (define-test unparse - (test-round-trip "#' () () ()") - (test-round-trip " (") - (loop :for string :being :the :hash-key :of *test-strings* - :do (test-round-trip string))) - #++ - (parachute:test 'unparse)) - +(define-test+run unparse + (test-round-trip "#' () () ()") + (test-round-trip " (") + (loop :for string :being :the :hash-key :of *test-strings* + :do (test-round-trip string))) +;; TODO make it easier to pin-point errors here... (define-test+run round-trip-breeze (loop :for file :in (breeze.asdf:system-files 'breeze) :for content = (alexandria:read-file-into-string file) - :do (test-round-trip content - :context file - ;; :check-for-error t - ))) + :do (let* ((state (parse content)) + (last-node (alexandria:lastcar (tree state))) + (result (unparse state nil))) + (walk state (lambda (node &rest args + &key depth + aroundp beforep afterp + firstp lastp nth + &allow-other-keys) + (declare (ignorable + args + depth + aroundp beforep afterp + firstp lastp nth)) + (unless (valid-node-p node) + ;; There's just too many nodes, this + ;; makes parachute completely choke if I + ;; don't filter the results... + (true (valid-node-p node) + "file: ~s node: ~s" file node)) + #++ (when (parens-node-p node) (char= #\()) + node)) + (is = (length content) (node-end last-node) + "Failed to parse correctly the file ~s. The last node is: ~s" + file + last-node) + #++ + (is = (length content) (length result) + "Round-tripping the file ~s didn't give the same length.") + (let ((mismatch (mismatch content result))) + (false mismatch "Failed to round-trip the file ~s. The first mismatch is at position: " + file + mismatch))))) diff --git a/tests/lossless-reader.randmized.lisp b/tests/lossless-reader.randmized.lisp new file mode 100644 index 00000000..c9656089 --- /dev/null +++ b/tests/lossless-reader.randmized.lisp @@ -0,0 +1,149 @@ + +(cl:in-package #:cl-user) + +(defpackage #:breeze.test.lossless-reader + (:documentation "Test package for #:breeze.lossless-reader") + (:use #:cl #:breeze.lossless-reader) + (:import-from #:breeze.lossless-reader + #:*state-control-string* + #:state-context + #:read-sharpsign-backslash + #:read-sharpsign-quote + #:read-sharpsign-left-parens + #:read-sharpsign-asterisk + #:read-sharpsign-colon + #:read-sharpsign-dot + #:read-sharpsign-b + #:read-sharpsign-o + #:read-sharpsign-x + #:read-sharpsign-r + #:read-sharpsign-c + #:read-sharpsign-a + #:read-sharpsign-s + #:read-sharpsign-p + #:read-sharpsign-equal + #:read-sharpsign-sharpsign + #:read-sharpsign-plus + #:read-sharpsign-minus + #:%token-symbol-node) + (:import-from #:parachute + #:define-test + #:define-test+run + #:is + #:true + #:false + #:of-type) + (:import-from #:breeze.kite + #:is-equalp* + #:is-equalp)) + +(in-package #:breeze.test.lossless-reader) + +(defvar *test-strings* (make-hash-table :test 'equal)) + +(defun register-test-string (string &optional (origin (list t)) &aux (ht *test-strings*)) + (let ((old-origin (gethash string ht))) + (unless (or (and (equal '(t) origin) + (member t old-origin)) + (and old-origin + (equal origin (alexandria:lastcar old-origin)))) + (setf (gethash string ht) + (append old-origin (alexandria:ensure-list origin))))) + string) + + + +;; Add _some_ randomized test strings +(defun wrap-in-parens (s) (format nil "(~a)" s)) +(defun wrap-in-block-comment (s) (format nil "#|~a|#" s)) +(defun prefix-with-line-comment (s) (format nil ";~a" s)) +;; reverse + +(defun randomize-1 (fn s origin) + (unless (equal (alexandria:lastcar origin) fn) + (funcall fn s))) + +(defun randomize-test-strings (&aux (before (hash-table-count *test-strings*))) + (loop + :for randomizer :in (list + 'reverse + 'wrap-in-parens + 'wrap-in-block-comment + 'prefix-with-line-comment) + :do + (loop + :for s :being :the :hash-key :of (alexandria:copy-hash-table *test-strings*) + :using (hash-value origin) + :for r = (randomize-1 randomizer s origin) + :when r + :do (register-test-string r randomizer))) + (format nil "~d new test strings generated (was ~d, now ~d)" + (- (hash-table-count *test-strings*) before) + before (hash-table-count *test-strings*))) + +#++ +(randomize-test-strings) + +#++ +(maphash (lambda (k v) + (unless (equal '(t) v) + (remhash k *test-strings*))) + *test-strings*) + +#++ +(remove-duplicates + (loop + :for s :being :the :hash-key :of *test-strings* + :using (hash-value origin) + :collect (alexandria:lastcar origin))) + +#++ +(remove-duplicates + (alexandria:hash-table-values *test-strings*) + :test 'equal) + +#++ +(alexandria:hash-table-keys *test-strings*) + +(define-test+run parse-randomized + (parachute:finish + (loop :for input :being :the :hash-key :of *test-strings* + :do (restart-case + (parse input) + (continue () + :report (lambda (stream) + (format stream "Continue to the next test."))) + (remove-and-continue () + :report (lambda (stream) + (format stream "Remove ~s from *test-strings* and continue to the next test." string)) + (remhash string *test-strings*)))))) + +(defmacro ∀ ((n &rest vars) &body body) + (if vars + (alexandria:once-only (n) + `(loop :for ,(first vars) :below ,n :do (∀ (,n ,@(rest vars)) ,@body))) + `(progn ,@body))) + +;; Generate all possible strings of 1 charcater +;; Generate all ASCII strings of length 2 and 3 +#++ +(define-test+run parse-exhaustive + (parachute:finish + (loop :for i :below char-code-limit + :do (parse (string (code-char i))))) + (parachute:finish + (∀ (256 x y) + (parse (map 'string 'code-char (list x y))))) + (let (input message) + (parachute:finish + (∀ (128 x y z) + (setf + ;; message (format nil "Failed with input x: ~s y: ~s z: ~s" x y z) + input (map 'string 'code-char (list x y z))) + (parse input)) + ;; "~a" message + ))) + +#++ +(∀ (128 x y z) + (map 'string 'code-char (list x y z))) diff --git a/tests/main.lisp b/tests/main.lisp index 3c9c4786..d3e4223d 100644 --- a/tests/main.lisp +++ b/tests/main.lisp @@ -7,14 +7,14 @@ (in-package #:breeze.test.main) +(defparameter cl-user::*exit-on-test-failures* nil) + (defun run-breeze-tests (&optional exitp) "Run breeze's tests." (let ((packages (breeze.xref:find-packages-by-prefix "breeze.test"))) (format *trace-output* - "~&About to run tests for the packages: ~{ - ~A~^~%~}" + "~&About to run tests for the packages:~%~{ - ~A~%~}" packages) (finish-output *trace-output*) - (let ((report (parachute:test packages))) - (if exitp - (uiop:quit (if (eq :failed (parachute:status report)) 1 0)) - report)))) + (let ((cl-user::*exit-on-test-failures* exitp)) + (parachute:test packages :report 'parachute:largescale)))) diff --git a/tests/pattern.lisp b/tests/pattern.lisp index ae9bebb4..a3061503 100644 --- a/tests/pattern.lisp +++ b/tests/pattern.lisp @@ -8,7 +8,8 @@ #:isnt #:true #:false - #:of-type) + #:of-type + #:fail) (:import-from #:breeze.pattern ;; Structures #:ref @@ -24,14 +25,14 @@ #:typed-term-name #:typed-term-type #:typed-term= + #:repetition + #:repetitionp + #:repetition= + #:repetition-pattern + #:repetition-min + #:repetition-max #:maybe - #:maybep - #:maybe-pattern - #:maybe= #:zero-or-more - #:zero-or-more-p - #:zero-or-more-pattern - #:zero-or-more= #:alternation #:alternationp #:alternation-pattern @@ -56,6 +57,8 @@ #:iterator-next #:iterator-value ;; Match + #:make-binding + #:merge-bindings #:match)) (in-package #:breeze.test.pattern) @@ -100,19 +103,15 @@ (define-test maybe (let ((maybe (maybe :x))) - (of-type maybe maybe) - (true (maybep maybe)) - (is eq :x (maybe-pattern maybe)))) - -;; TODO maybe= + (of-type repetition maybe) + ;; TODO check repetition-{min,max} + (is eq :x (repetition-pattern maybe)))) (define-test zero-or-more (let ((zero-or-more (zero-or-more :x))) - (of-type zero-or-more zero-or-more) - (true (zero-or-more-p zero-or-more)) - (is eq :x (zero-or-more-pattern zero-or-more)))) - -;; TODO zero-or-more= + (of-type repetition zero-or-more) + ;; TODO check repetition-{min,max} + (is eq :x (repetition-pattern zero-or-more)))) (define-test alternation (let ((alternation (alternation :x))) @@ -158,11 +157,16 @@ (is pattern= (term :?x) (compile-pattern :?x)) (is pattern= (ref :x) (compile-pattern '(:ref :x))) (is pattern= (maybe :x) (compile-pattern '(:maybe :x))) - (is pattern= (maybe #(:x :y)) (compile-pattern '(:maybe :x :y))) - (is pattern= (zero-or-more :x) (compile-pattern '(:zero-or-more :x))) + (is pattern= (maybe :x :?y) (compile-pattern '(:maybe :x :?y))) + (is pattern= (maybe #(:x :y)) (compile-pattern '(:maybe (:x :y)))) + (is pattern= (zero-or-more #(:x)) (compile-pattern '(:zero-or-more :x))) (is pattern= (zero-or-more #(:x :y)) (compile-pattern '(:zero-or-more :x :y))) - (is pattern= (alternation :x) (compile-pattern '(:alternation :x))) - (is pattern= (alternation #(:x :y)) (compile-pattern '(:alternation :x :y)))) + (is pattern= (alternation #(:x)) (compile-pattern '(:alternation :x))) + (is pattern= (alternation #(:x :y)) (compile-pattern '(:alternation :x :y))) + (multiple-value-bind (p terms) + (compile-pattern '(?x ?x)) + (is eq (aref p 0) (aref p 1)) + (is eq (aref p 0) (gethash '?x terms)))) @@ -186,12 +190,23 @@ ;; I _cannot_ iterate over both in at the same speed. +(defun test-iterator (iterator vector + &key (pos 0) donep value) + (is eq vector (iterator-vector iterator) + "The iterator was not intialized with the right vector.") + (is = pos (iterator-position iterator) + "The iterator's position was not correctly initialized to ~s." pos) + (is = 1 (iterator-step iterator) + "The iterator's step was not correctly initialized to 1.") + (when donep + (true (iterator-done-p iterator))) + (when value + (is pattern= value (iterator-value iterator)))) + (define-test make-iterator (let* ((vector #(1 2 3)) (iterator (make-iterator :vector vector))) - (is eq vector (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator)))) + (test-iterator iterator vector))) (define-test iterator-done-p (true (iterator-done-p (make-iterator :vector #()))) @@ -201,112 +216,105 @@ (false (iterator-done-p (make-iterator :vector #(1 2 3)))) (true (iterator-done-p (make-iterator :vector #(1 2 3) :position 10)))) -#++ (define-test iterator-push (let* ((vector1 #(1 2 3)) (vector2 #(a b c d e f)) (iterator (iterator-push (make-iterator :vector vector1) vector2))) - (is eq vector2 (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator)))) + (test-iterator iterator vector2))) -#++ (define-test iterator-maybe-push ;; empty case, so the iterator is donep from the start (let* ((vector #()) (iterator (iterator-maybe-push (make-iterator :vector vector)))) - (is eq vector (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator))) + (test-iterator iterator vector)) ;; non-empty, no ref (let* ((vector #(1 2 3)) (iterator (iterator-maybe-push (make-iterator :vector vector)))) - (is eq vector (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator))) + (test-iterator iterator vector)) ;; starts with a ref (let* ((ref (ref 'a)) (vector `#(,ref)) (root-iterator (make-iterator :vector vector)) (iterator (iterator-maybe-push root-iterator))) (isnt eq root-iterator iterator) - (is eq (ref-pattern ref) (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator)) + (test-iterator iterator (ref-pattern ref)) (is pattern= 'a (iterator-value iterator)))) -#++ +;; This also tests iterator-maybe-{push,pop} (define-test iterator-next ;; empty case, so the iterator is donep from the start (let* ((vector #()) - (iterator (iterator-next (iterator-maybe-push (make-iterator :vector vector))))) - ;; TODO check done-p - ;; TODO check value - (is eq vector (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator))) - ;; non-empty, no ref - (let* ((vector #(1 2 3)) - (iterator (iterator-next (iterator-maybe-push (make-iterator :vector vector))))) - ;; TODO - (is eq vector (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator))) - ;; starts with a ref - (let* ((ref (ref 'a)) - (vector `#(,ref)) - (root-iterator (make-iterator :vector vector)) - (iterator (iterator-next (iterator-maybe-push root-iterator)))) - ;; TODO - (isnt eq root-iterator iterator) - (is eq (ref-pattern ref) (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator)) - (is pattern= 'a (iterator-value iterator)))) - -#++ -(define-test iterator-maybe-pop - ;; empty case - (let* ((vector #()) - (iterator (iterator-maybe-pop (iterator-maybe-push (make-iterator :vector vector))))) - (is eq vector (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator))) + (iterator (iterate vector))) + (test-iterator iterator vector :pos 0 :donep t) + (parachute:fail (iterator-value iterator)) + (iterator-next iterator) + (test-iterator iterator vector :pos 1 :donep t) + (fail (iterator-value iterator))) ;; non-empty, no ref (let* ((vector #(1 2 3)) - (iterator (iterator-maybe-pop (iterator-maybe-push (make-iterator :vector vector))))) - (is eq vector (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator))) -;;;; WIP + (iterator (iterate vector))) + (test-iterator iterator vector :pos 0 :value 1) + (iterator-next iterator) + (test-iterator iterator vector :pos 1 :value 2)) ;; starts with a ref (let* ((ref (ref 'a)) (vector `#(,ref)) (root-iterator (make-iterator :vector vector)) (iterator (iterator-maybe-push root-iterator))) - (iterator-next) (isnt eq root-iterator iterator) - (is eq (ref-pattern ref) (iterator-vector iterator)) - (is = 0 (iterator-position iterator)) - (is = 1 (iterator-step iterator)) - (is pattern= 'a (iterator-value iterator))) - ) - -#++ -(defun test-iterator (vector) + (test-iterator root-iterator vector) + ;;; We're referencing the pattern '(a ?a) + ;; check the first value + (test-iterator iterator (ref-pattern ref) :pos 0 :value 'a) + ;; advance the iterator + (setf iterator (iterator-next iterator)) ; maybe a macro for this? (nextf iterator) + ;; check the second value + (test-iterator iterator (ref-pattern ref) :pos 1 :value #S(term :name ?a)) + ;; (is pattern= #S(term :name ?a) (iterator-value iterator)) + ;; advance the iterator + (let ((iterator2 (iterator-next iterator))) + (test-iterator iterator (ref-pattern ref) :pos 2 :donep t) + (isnt eq iterator iterator2 "iterator-next should have returned a different iterator.") + (is eq root-iterator iterator2 "iterator-next should have returned the root iterator.") + (test-iterator iterator2 vector :pos 1 :donep t) + (fail (iterator-value iterator2))))) + +;; TODO This _could_ be renamed "flatten pattern" ? +(defun test-iterator* (vector) (loop + :for i :from 0 :for iterator := (iterate vector) :then (iterator-next iterator) - :until (iterator-done-p iterator) + :until (prog1 (iterator-done-p iterator) + ;; (format *debug-io* "~%~%~d: ~S" i iterator) + ) :for value = (iterator-value iterator) - :do (format *debug-io* "~%~%~S~%~%" value) + ;; :do (format *debug-io* "~&~d: ~S~%~%" i value) :collect value)) -#++ -(test-iterator `#(,(ref 'a))) + +(define-test iterator + (is equalp '(a #s(term :name ?a)) + (test-iterator* `#(,(ref 'a)))) + (is equalp '(a #s(term :name ?a) a #s(term :name ?a)) + (test-iterator* `#(,(ref 'b))))) +(define-test merge-bindings + (false (merge-bindings nil nil)) + (false (merge-bindings nil t)) + (false (merge-bindings t nil)) + (true (merge-bindings t t)) + (false (merge-bindings (make-binding :?x 'a) nil)) + (false (merge-bindings nil (make-binding :?x 'a))) + (is equal '((:?x . a)) (merge-bindings (make-binding :?x 'a) t)) + (is equal '((:?x . a)) (merge-bindings t (make-binding :?x 'a))) + (is equal '((:?x . a) (:?y . b)) (merge-bindings (make-binding :?x 'a) (make-binding :?y 'b))) + (let ((term (term 'a))) + (is equal `((,term . 42)) + (merge-bindings `((,term . 42)) `((,term . 42)))))) + (defun test-match (pattern input) (match (compile-pattern pattern) input)) @@ -319,11 +327,9 @@ (false (match 1 2)) (true (match 'x 'x)) (true (match "x" "x")) - (false (match 'x 'y)) - (true (match #(a) '(a))) - ;; TODO add vectors (but not arrays) - ) + (false (match 'x 'y))) +;;; TODO check the actual return values (define-test "match terms" (true (match (term :?x) nil)) (true (match (term :?x) 1)) @@ -333,6 +339,7 @@ (true (match (term :?x) (term :?x))) (true (match `#(,(term :?x)) (list 42)))) +;;; TODO check the actual return values (define-test "match typed-terms" (true (match (typed-term 'null :?x) nil)) (false (match (typed-term 'null :?x) t)) @@ -346,6 +353,60 @@ (false (match (typed-term 'cons :?x) 'a))) +;;; Sequences + +(define-test+run "match sequences" + (true (match #(a) '(a))) + (false (match #(a b) #(a))) + (true (match #(a b) #(a b))) + (false (match #(a b) #(a b a)))) + + +;;; test :maybe :zero-or-more and :alternation + +#++ ;; TODO +(define-test "match maybe" + (is eq t (match (maybe 'a) 'a)) + (is eq t (match (maybe 'a) nil)) + (is eq t (match (maybe 'a :?x) nil)) + (false (match (maybe 'a :?x) 'b)) + (false (match (maybe 'a) 'b)) + (is equalp `(,(maybe :name :?x :pattern a) a) (match (maybe 'a :?x) 'a)) + (is equalp '(#(term :name '?x) a) (match (maybe (term '?x)) 'a)) + (is equalp `(,(term :name '?x) nil) (match (maybe (term '?x)) nil))) + +#++ ;; TODO +(define-test "match alternations" + (is eq t (test-match '(:alternation a b) 'a)) + (is eq t (test-match '(:alternation a b) 'b)) + (false (test-match '(:alternation a b) 'c)) + (is equalp '(#s(term :name ?x) c) (test-match '(:alternation ?x b) 'c)) + (let ((pat (compile-pattern '(:alternation (:maybe a ?x) b)))) + (is equalp `(,(maybe :name ?x :pattern a) a) (test-match pat 'a)) + (is eq t (test-match pat 'b)) + (false (test-match pat 'c)))) + +#++ ;; TODO +(define-test+run "match zero-or-more" + (true (test-match '(:zero-or-more a) nil)) + (false (test-match '(:zero-or-more a b) '(a))) + (is eq t (test-match '(:zero-or-more a b) '(a b))) + (false (test-match '(:zero-or-more a b) '(a b a))) + (is eq t (test-match '(:zero-or-more a b) '(a b a b))) + (false (test-match '(:zero-or-more a b) 'a))) + +#++ ;; TODO +(progn + ;; I want this to be true + (test-match '(a (:zero-or-more a b)) '(a a b)) + ;; Not this + (test-match '(a (:zero-or-more a b)) '(a (a b))) + ;; That one should be used instead of ^^^ + (test-match '(a ((:zero-or-more a b))) '(a (a b)))) + + + +;;; Testing patterns with references in them (defpattern optional-parameters &optional @@ -354,6 +415,22 @@ ((:the symbol ?var) ?init-form (:maybe (:the symbol ?supplied-p-parameter)))))) +#++ +(match (ref 'optional-parameters) + '(&optional)) + +#++ +(match (ref 'optional-parameters) + '(&optional x)) + +#++ +(list + '(&optional x) + '(&optional (x 1)) + '(&optional (x 1 supplied-p)) + '(&optional x y (z t))) + + (defpattern rest-parameter &rest ?var) (defpattern body-parameter &body ?var) @@ -382,28 +459,56 @@ (defpattern defun (defun (:the symbol ?name) $ordinary-lambda-list ?body)) -#++ -(match 'optional-parameters - '(&optional)) -#++ -(list - '(&optional x) - '(&optional (x 1)) - '(&optional (x 1 supplied-p)) - '(&optional x y (z t))) + + + +(defun test-match-ref (pattern input &key bindings) + (let ((result (match pattern input))) + (if bindings + (is equalp bindings (if (listp result) + (mapcar (lambda (x) + (cons (term-name (car x)) (cdr x))) + result) + result) + "Matching the pattern ~s agains the input ~s should have created the bindings ~s but we got ~s instead." + pattern input bindings result) + (false result)))) + + +(define-test+run "match ref" + (test-match-ref (ref 'a) '(a 42) :bindings '((?a . 42))) + (test-match-ref (ref 'b) '(a 42 a 73)) + ;; TODO What if we want to use the pattern 'a with independent bindings? + ;; Idea new syntax: (ref 'a ('?a ?a1)) or (ref 'a :prefix a1) + (test-match-ref (ref 'b) '(a 42 a 42) :bindings '((?a . 42))) + (test-match-ref (ref 'body-parameter) '(42)) + (test-match-ref (ref 'body-parameter) '(&body 42) + :bindings '((?var . 42)))) + + + +;;; Match substitution + +(defun test-pattern-substitute (pattern bindings) + (multiple-value-bind (compiled-pattern term-pool) + (breeze.pattern:compile-pattern pattern) + (let ((actual-bindings + (sublis (alexandria:hash-table-alist term-pool) bindings))) + (pattern-substitute compiled-pattern actual-bindings)))) +;;; Rules and rewrites -;; (trace match :methods t) #++ -(define-test "match ref" - (true (match `#(,(ref 'a)) '(a 42))) - (true (match `#(,(ref 'b)) '(a 42 a 73))) - (false (match `#(,(ref 'body-parameter)) '(42))) - (true (match `#(,(ref 'body-parameter)) '(&body 42)))) - -;; TODO I tested if match return the right generalized boolean, but I -;; haven't tested the actual value it returns when it's true. Which -;; should be either t or a list of new bindings. +(let ((r (make-rewrite '(/ ?x ?x) 1))) + (list (pattern= (rewrite-pattern r) #(/ (term :?x) (term :?x))) + (rewrite-template r))) + +#++ +(make-rewrite '(/ (* ?x ?y) ?z) + '(* ?x (/ ?y ?z))) + +#++ +(make-rewrite '(/ ?x 1) ?x) diff --git a/tests/refactor.lisp b/tests/refactor.lisp index caeb34ed..9b50fa88 100644 --- a/tests/refactor.lisp +++ b/tests/refactor.lisp @@ -1,7 +1,7 @@ (cl:in-package #:common-lisp-user) (uiop:define-package #:breeze.test.refactor - (:use :cl #:breeze.refactor) + (:use :cl #:breeze.refactor) ;; Importing non-exported symbols of the "package under test" (:import-from #:breeze.refactor) ;; Things needed to "drive" a command @@ -20,29 +20,9 @@ #:outer-node) (:import-from #:breeze.test.command #:drive-command) - #++ - (:import-from #:breeze.reader - #:node-content - #:parse-string - #:unparse-to-string - - ;; Types of node - #:skipped-node - #:symbol-node - #:read-eval-node - #:character-node - #:list-node - #:function-node - - ;; Type predicates - #:skipped-node-p - #:symbol-node-p - #:read-eval-node-p - #:character-node-p - #:list-node-p - #:function-node-p) (:import-from #:breeze.utils - #:remove-indentation) + #:remove-indentation + #:split-by-newline) (:import-from #:parachute #:define-test #:define-test+run @@ -84,6 +64,7 @@ commands))) + ;;; Oh yiisss! In this "page", I create a command that can generate ;;; tests _interactively_ for a command. ;;; @@ -98,19 +79,20 @@ newline in the expected result." (progn (insert "~% (is equal") (insert "~% '(") - (loop :for line :in (str:split #\Newline expected) + (loop :for line :in (split-by-newline expected) :for i :from 0 :unless (zerop i) :do (insert "~% ") :do (insert "~s" line)) (insert ")") - (insert "~% (str:split #\\Newline (~:R request)))" i)) + (insert "~% (split-by-newline (~:R request)))" i)) (insert "~% (is string= ~s (~:R request))" expected i)) (insert "~% (false (~:R request))" i))) ;; TODO I sorely need something more declarative for those kinds of ;; snippets... Which is why I'm working so much on having good tests ;; for the snippets in the first place! +#++ (define-command insert-test () "Insert a missing test!" (augment-context-by-parsing-the-buffer (breeze.command:context*)) @@ -177,7 +159,7 @@ newline in the expected result." ;; This is emacs lisps to add a binding to the command "insert-test" ;; defined just above: -#++ +#+elisp (progn (defun breeze--insert-test () (interactive) @@ -225,7 +207,7 @@ newline in the expected result." '("(cl:in-package #:cl)" "" "") - (str:split #\Newline (second request)))) + (split-by-newline (second request)))) (destructuring-bind (input request) (fifth trace) (false input) (is string= "insert" (first request)) @@ -234,7 +216,7 @@ newline in the expected result." " (:use :cl :asdf))" "" "") - (str:split #\Newline (second request)))) + (split-by-newline (second request)))) (destructuring-bind (input request) (sixth trace) (false input) (is string= "insert" (first request)) @@ -242,7 +224,7 @@ newline in the expected result." '("(in-package #:a.asd)" "" "") - (str:split #\Newline (second request)))) + (split-by-newline (second request)))) (destructuring-bind (input request) (seventh trace) (false input) (is string= "insert" (first request)) @@ -258,7 +240,7 @@ newline in the expected result." " :components" " (#+(or) (:file \"todo\")))" "") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) (define-test+run insert-breeze-define-command (let* ((trace (drive-command #'insert-breeze-define-command @@ -276,38 +258,35 @@ newline in the expected result." '("(define-command rmrf ()" " \"Rmrf.\"" " )") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) (define-test+run insert-defun - (let* ((trace (drive-command #'insert-defun - :inputs '("real-fun" "a &optional b") - :context '()))) - (common-trace-asserts 'insert-defun trace 7) - (destructuring-bind (input request) (first trace) - (is string= "insert" (first request)) - (is string= "(defun " (second request))) - (destructuring-bind (input request) (second trace) - (is string= "read-string" (first request)) - (is string= "Name: " (second request)) - (is string= nil (third request))) - (destructuring-bind (input request) (third trace) - (is equal '"real-fun" input) - (is string= "insert" (first request)) - (is string= "real-fun (" (second request))) - (destructuring-bind (input request) (fourth trace) - (is string= "read-string" (first request)) - (is string= "Enter the arguments: " (second request)) - (is string= nil (third request))) - (destructuring-bind (input request) (fifth trace) - (is equal '"a &optional b" input) - (is string= "insert" (first request)) - (is equal - '("a &optional b)" - ")") - (str:split #\Newline (second request)))) - (destructuring-bind (input request) (sixth trace) - (is string= "backward-char" (first request)) - (is string= nil (second request))))) + (let* ((trace (drive-command #'insert-defun + :inputs '("real-fun" "a &optional b") + :context '()))) + (common-trace-asserts 'insert-defun trace 6) + (destructuring-bind (input request) (first trace) + (is string= "insert" (first request)) + (is string= "(defun " (second request))) + (destructuring-bind (input request) (second trace) + (is string= "read-string" (first request)) + (is string= "Name: " (second request)) + (is string= nil (third request))) + (destructuring-bind (input request) (third trace) + (is equal '"real-fun" input) + (is string= "insert" (first request)) + (is string= "real-fun (" (second request))) + (destructuring-bind (input request) (fourth trace) + (is string= "read-string" (first request)) + (is string= "Enter the arguments: " (second request)) + (is string= nil (third request))) + (destructuring-bind (input request) (fifth trace) + (is equal '"a &optional b" input) + (is string= "insert" (first request)) + (is equal + '("a &optional b)" + ")") + (split-by-newline (second request)))))) (define-test insert-defvar @@ -336,7 +315,7 @@ newline in the expected result." (is equal '("42" "") - (str:split #\Newline (second request)))) + (split-by-newline (second request)))) (destructuring-bind (input request) (sixth trace) (is string= "read-string" (first request)) (is string= "Documentation string " (second request)) @@ -365,13 +344,13 @@ newline in the expected result." " :initarg :slot" " :accessor klass-slot))" " (:documentation \"\"))") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) (define-test insert-defmacro (let* ((trace (drive-command #'insert-defmacro :inputs '("mac" "(x) &body body") :context '()))) - (common-trace-asserts 'insert-defmacro trace 7) + (common-trace-asserts 'insert-defmacro trace 6) (destructuring-bind (input request) (first trace) (is string= "insert" (first request)) (is string= "(defmacro " (second request))) @@ -393,29 +372,26 @@ newline in the expected result." (is equal '("(x) &body body)" ")") - (str:split #\Newline (second request)))) - (destructuring-bind (input request) (sixth trace) - (is string= "backward-char" (first request)) - (is string= nil (second request))))) + (split-by-newline (second request)))))) (define-test+run insert-defgeneric - (let* ((trace (drive-command #'insert-defgeneric - :inputs '("gen") - :context '()))) - (common-trace-asserts 'insert-defgeneric trace 3) - (destructuring-bind (input request) (first trace) - (is string= "read-string" (first request)) - (is string= "Name of the generic function: " (second request)) - (is string= nil (third request))) - (destructuring-bind (input request) (second trace) - (is equal '"gen" input) - (is string= "insert" (first request)) - (is equal - '("(defgeneric gen ()" - " (:documentation \"\")" - " #++(:method-combination + #++ :most-specific-last)" - " (:method () ()))") - (str:split #\Newline (second request)))))) + (let* ((trace (drive-command #'insert-defgeneric + :inputs '("gen") + :context '()))) + (common-trace-asserts 'insert-defgeneric trace 3) + (destructuring-bind (input request) (first trace) + (is string= "read-string" (first request)) + (is string= "Name of the generic function: " (second request)) + (is string= nil (third request))) + (destructuring-bind (input request) (second trace) + (is equal '"gen" input) + (is string= "insert" (first request)) + (is equal + '("(defgeneric gen ()" + " (:documentation \"\")" + " #++(:method-combination + #++ :most-specific-last)" + " (:method () ()))") + (split-by-newline (second request)))))) (define-test insert-defmethod (let* ((trace (drive-command #'insert-defmethod @@ -432,7 +408,7 @@ newline in the expected result." (is equal '("(defmethod frob ()" " )") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) ;; TODO Variants: *insert-defpackage/cl-user-prefix* ;; TODO infer-project-name @@ -462,49 +438,49 @@ newline in the expected result." " (:use #:cl))" "" "(in-package #:pkg)") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) (define-test+run insert-defparameter - (let* ((trace (drive-command #'insert-defparameter - :inputs '("param" "\"meh\"" - "This is a meh variable") - :context '()))) - (common-trace-asserts 'insert-defparameter trace 8) - (destructuring-bind (input request) (first trace) - (false input) - (is string= "insert" (first request)) - (is string= "(defparameter " (second request))) - (destructuring-bind (input request) (second trace) - (false input) - (is string= "read-string" (first request)) - (is string= "Name: " (second request)) - (false (third request))) - (destructuring-bind (input request) (third trace) - (is string= "param" input) - (is string= "insert" (first request)) - (is string= "*param* " (second request))) - (destructuring-bind (input request) (fourth trace) - (false input) - (is string= "read-string" (first request)) - (is string= "Initial value: " (second request)) - (false (third request))) - (destructuring-bind (input request) (fifth trace) - (is string= "\"meh\"" input) - (is string= "insert" (first request)) - (is equal - '("\"meh\"" - "") - (str:split #\Newline (second request)))) - (destructuring-bind (input request) (sixth trace) - (false input) - (is string= "read-string" (first request)) - (is string= "Documentation string " (second request)) - (false (third request))) - (destructuring-bind (input request) (seventh trace) - (is string= "This is a meh variable" input) - (is string= "insert" (first request)) - (is string= "\"This is a meh variable\")" (second request))))) + (let* ((trace (drive-command #'insert-defparameter + :inputs '("param" "\"meh\"" + "This is a meh variable") + :context '()))) + (common-trace-asserts 'insert-defparameter trace 8) + (destructuring-bind (input request) (first trace) + (false input) + (is string= "insert" (first request)) + (is string= "(defparameter " (second request))) + (destructuring-bind (input request) (second trace) + (false input) + (is string= "read-string" (first request)) + (is string= "Name: " (second request)) + (false (third request))) + (destructuring-bind (input request) (third trace) + (is string= "param" input) + (is string= "insert" (first request)) + (is string= "*param* " (second request))) + (destructuring-bind (input request) (fourth trace) + (false input) + (is string= "read-string" (first request)) + (is string= "Initial value: " (second request)) + (false (third request))) + (destructuring-bind (input request) (fifth trace) + (is string= "\"meh\"" input) + (is string= "insert" (first request)) + (is equal + '("\"meh\"" + "") + (split-by-newline (second request)))) + (destructuring-bind (input request) (sixth trace) + (false input) + (is string= "read-string" (first request)) + (is string= "Documentation string " (second request)) + (false (third request))) + (destructuring-bind (input request) (seventh trace) + (is string= "This is a meh variable" input) + (is string= "insert" (first request)) + (is string= "\"This is a meh variable\")" (second request))))) (define-test insert-handler-bind-form (let* ((trace (drive-command #'insert-handler-bind-form @@ -519,7 +495,7 @@ newline in the expected result." " ((error #'(lambda (condition)" " (describe condition *debug-io*))))" " (frobnicate))") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) (define-test insert-handler-case-form (let* ((trace (drive-command #'insert-handler-case-form @@ -534,7 +510,7 @@ newline in the expected result." " (frobnicate)" " (error (condition)" " (describe condition *debug-io*)))") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) (define-test insert-in-package-cl-user (let* ((trace (drive-command #'insert-in-package-cl-user @@ -672,7 +648,7 @@ newline in the expected result." " (print-unreadable-object" " (node stream :type t :identity nil)" " (format stream \"~s\" (node-something node))))") - (str:split #\Newline (second request)))))) + (split-by-newline (second request)))))) ;; TODO (define-test+run insert-parachute-define-test) @@ -765,6 +741,6 @@ strings get concatenated." #+ (or) -(context-buffer-string +(buffer-string (alexandria:plist-hash-table '(:buffer-string "asdf"))) diff --git a/tests/xref.lisp b/tests/xref.lisp index bcc33658..3d43bb73 100644 --- a/tests/xref.lisp +++ b/tests/xref.lisp @@ -14,7 +14,7 @@ (define-test find-package (is equal (find-packages-by-prefix "breeze") - (find-packages-by-regex "breeze.*"))) + (find-packages-by-prefix "breeze."))) (defparameter *symbols* '(dum:*bound-variable* @@ -79,7 +79,7 @@ (with-output-to-string (*standard-output*) (loop :for symbol :being :the :external-symbol :of 'breeze.dummy.test :for pass = (funcall fn symbol) - :unless (str:containsp "undocumented" (string-downcase (symbol-name symbol))) + :unless (search "undocumented" (string-downcase (symbol-name symbol))) :do (format t "~&(is ") (unless pass (format t "(not ")) diff --git a/workbench.el b/workbench.el new file mode 100644 index 00000000..e8d9861d --- /dev/null +++ b/workbench.el @@ -0,0 +1,72 @@ + +(setf debug-on-error t) +(setf debug-on-error nil) + +;; Useful for debugging whether slime or sly is running +(process-list) + + +;;; Reloading + +(breeze-eval "(asdf:load-system '#:breeze :force t)") +(load breeze-breeze.el) + + +;;; Listener + +(breeze-list-loaded-listeners) + +(breeze-choose-listener) + +(breeze-check-if-listener-connected) + +(completing-read "Choose a lisp listener to start: " + '(sly slime) nil t) + + +;;; Eval + +(breeze-eval "1") +(breeze-eval "'(a b c)") +(breeze-eval "t") +(breeze-eval "(not nil)") + + + +;;; Initialization + +(breeze-validate-if-package-exists "cl") + +(breeze-validate-if-package-exists "breeze") + +(breeze-validate-if-breeze-package-exists) + + +;;; "Dynamic" emacs commands + +(breeze-translate-command-lambda-list '(a b c)) + +(breeze-translate-command-lambda-list '(a::1 b:2 c::3)) + +(breeze-refresh-commands) + +(symbol-function 'breeze-scaffold-project) + + +;;; Trying to make breeze system load automatically... and +;;; asynchronously if it make sense. + +(breeze-add-hooks 'slime) +(breeze-add-hooks 'sly) ; not implemented yet + + +;;; Other listener hooks + +slime-connected-hook +slime-inferior-process-start-hook +slime-net-process-close-hooks +slime-cycle-connections-hook +slime-connected-hook +slime-event-hooks + +;; contribs (like slime-repl) defines even more hooks diff --git a/workbench.lisp b/workbench.lisp index ceb0563d..ef4d6cc5 100644 --- a/workbench.lisp +++ b/workbench.lisp @@ -25,6 +25,30 @@ ;; Kill all currently running breeze-commands (kill-threads-by-name "breeze command handler") + +(defvar *default-trace-report-default* sb-debug:*trace-report-default*) + +;; tracing is very very useful for debugging, but the default way sbcl +;; often prints "way too much" stuff +(defun trace-report (depth function event stack-frame values) + ;; (pprint-logical-block stream values :prefix ... :suffix ...) + (let ((*print-pretty* nil) + (stream *standard-output*)) + (terpri stream) + (pprint-logical-block (stream values + :per-line-prefix (format nil "~v@{~A~:*~}" depth " |")) + ;; (loop :repeat depth :do (format stream " |")) + (pprint-indent :current depth stream) + (case event + (:enter + (format stream "~3d (~a ~{~a~^ ~})" depth function values)) + (:exit + (format stream "~3d => ~{~a~^, ~}" depth values)) + (t + (format stream "~3d ~s (~a ~{~a~^ ~})" depth event function values)))))) + +(setf sb-debug:*trace-report-default* 'trace-report) + (setf *break-on-signals* 'error) @@ -95,6 +119,26 @@ (context *a*) +;;; Prototyping the request thingy... + +(request 'x) +;; => nil + +(handler-bind + ((request #'(lambda (request) + ;; (format t "~%request: ~s" request) + (if (eq (what request) 'x) + (answer 42) + (signal request))))) + (mapcar (lambda (what) + (multiple-value-list (request what))) + '(x y))) +;; => ((42 T) (NIL)) + +(with-answers + ()) + + ;; refactor.lisp @@ -113,11 +157,11 @@ ) (let* ((*standard-output* *debug-io*) - (nodes ) - (path ) - (outer-node ) - (parent-node ) - (inner-node )) + (nodes) + (path) + (outer-node) + (parent-node) + (inner-node)) (loop :for (node . index) :in path :for i :from 0 :do (format t "~%=== Path part #~d, index ~d ===~%~s" @@ -163,15 +207,125 @@ ;;; lossless-reader.lisp +(in-package #:breeze.lossless-reader) + (trace read-string* - read-char*) + read-char* + read-while) (trace - %read-whitespaces - %read-block-comment - %read-token + read-whitespaces + read-block-comment + read-line-comment + read-sharpsign-dispatching-reader-macro + read-punctuation + ;; read-quoted-string + read-string + read-token read-parens - read-extraneous-closing-parens) + read-extraneous-closing-parens + read-any + parse) + +(untrace) + + + + +(in-package #:breeze.pattern) + +(trace iterator-next + iterator-maybe-push + iterator-maybe-pop) + +(trace merge-bindings) + +(trace match) + +(untrace) + + +(in-package #:breeze.test.pattern) + +(test-match '(:zero-or-more a b) '(a b a b)) + +(trace compile-pattern) + +(trace match :methods t) + +(trace :wherein test-match-ref + ;; match + merge-bindings) + + +(in-package #:breeze.test.analysis) + +(trace in-package-node-p + :wherein test-in-package-node-p) + +(trace :wherein test-lint + in-package-node-p) + +(trace match + :wherein test-in-package-node-p) + +(trace + :wherein test-match-parse + match + breeze.analysis::match-symbol-to-token + breeze.analysis::match-node) (untrace) + + +(trace lint) + + + +(in-package #:breeze.listener) + +(trace suggest-symbol + suggest-package + suggest-class) + + + +(progn + ;; List the slot of a condition + (sb-kernel::condition-assigned-slots *condition*) + + ;; Get the first element of a condition's format arguments + (car + (slot-value *condition* + 'sb-kernel::format-arguments))) + + + +(defparameter *condition* *last-condition* + "Just a quick way to save the last-condition.") + + + +#+ (or) +(type-of *condition*) +;; => SB-PCL::MISSING-SLOT + + + +(prin t) +(commmon-lisp:print :oups) +(cl:prin :oups) +(call-with-correction-suggestion (lambda () (eval '(prin)))) +(make-instance 'typos) + + + + +#| + +TODO Would be nice to have a "Shadow-import all" restart. + +|# + +