Skip to content

Add first implementation of the Emacs Lisp representer #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/tests/**/*/representation.txt
/tests/**/*/representation.json
/tests/**/*/mapping.json
src/elpa
*.elc
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
FROM alpine:3.18.3
FROM silex/emacs:30.1-alpine

# TODO: install packages required to run the representer
# TODO(FAP): install emacs package dependencies during Docker image build?

# TODO(FAP): either use jq or don't install
RUN apk add --no-cache bash jq

WORKDIR /opt/representer
Expand Down
36 changes: 1 addition & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,4 @@
# Exercism Representer Template

This repository is a [template repository](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository) for creating [representers][representers] for [Exercism][exercism] tracks.

## Using the Representer Template

1. Ensure that your track has not already implemented a representer. If there is, there will be a `https://github.com/exercism/<track>-representer` repository (i.e. if your track's slug is `python`, the representer repo would be `https://github.com/exercism/python-representer`)
2. Follow [GitHub's documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) for creating a repository from a template repository
- Name your new repository based on your language track's slug (i.e. if your track is for Python, your representer repo name is `python-representer`)
3. Remove this [Exercism Representer Template](#exercism-representer-template) section from the `README.md` file
4. Replace `TRACK_NAME_HERE` with your track's name in the `README.md` file
5. Replace any occurances of `exercism/representer` with `exercism/<track>-representer` (e.g. `exercism/python-representer`)
6. Build the representer, conforming to the [Representer interface specification](https://github.com/exercism/docs/blob/main/building/tooling/representers/interface.md).
- Update the files to match your track's needs. At the very least, you'll need to update `bin/run.sh`, `Dockerfile` and the test solutions in the `tests` directory.
The existing test solutions are suggestions for you could be testing, but remember to add your track-specific files too
- Tip: look for `TODO:` comments to point you towards code that need updating
- Tip: look for `OPTIONAL:` comments to point you towards code that _could_ be useful

Once you're happy with your representer, [open an issue on the exercism/exercism repo](https://github.com/exercism/exercism/issues/new?title=%5BTRACK%5D+Request+Representer+Repository) to request an official representer repository for your track.

## Default Implementation

The default implementation works as follows:

- The `representation.txt` contains the concatenated solution files
- Solution files are separated by an empty line
- Solution files are identified via the the `.files.solution[]` property in the `.meta/config.json` file
- The `mapping.json` contains an empty JSON object (`{}`)

### Normalizations

- Blank files in the solution files are removed in the `representation.txt`
- Line-based trailing whitespace in the solution files is removed in the `representation.txt`

# Exercism TRACK_NAME_HERE Representer
# Exercism Emacs Lisp Representer

The Docker image to automatically create a representation for TRACK_NAME_HERE solutions submitted to [Exercism].

Expand Down
4 changes: 2 additions & 2 deletions bin/run-in-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ output_dir="${3%/}"
mkdir -p "${output_dir}"

# Build the Docker image
docker build --rm -t exercism/representer .
docker build --rm -t exercism/emacs-lisp-representer .

# Run the Docker image using the settings mimicking the production environment
docker run \
Expand All @@ -40,4 +40,4 @@ docker run \
--mount type=bind,source="${input_dir}",destination=/solution \
--mount type=bind,source="${output_dir}",destination=/output \
--mount type=tmpfs,destination=/tmp \
exercism/representer "${slug}" /solution /output
exercism/emacs-lisp-representer "${slug}" /solution /output
4 changes: 2 additions & 2 deletions bin/run-tests-in-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# ./bin/run-tests-in-docker.sh

# Build the Docker image
docker build --rm -t exercism/representer .
docker build --rm -t exercism/emacs-lisp-representer .

# Run the Docker image using the settings mimicking the production environment
docker run \
Expand All @@ -25,4 +25,4 @@ docker run \
--volume "${PWD}/bin/run-tests.sh:/opt/representer/bin/run-tests.sh" \
--workdir /opt/representer \
--entrypoint /opt/representer/bin/run-tests.sh \
exercism/representer
exercism/emacs-lisp-representer
12 changes: 5 additions & 7 deletions bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,16 @@ output_dir="${3%/}"
representation_txt_file="${output_dir}/representation.txt"
representation_json_file="${output_dir}/representation.json"
mapping_file="${output_dir}/mapping.json"
representer_dir=src

# Create the output directory if it doesn't exist
mkdir -p "${output_dir}"

echo "${slug}: creating representation..."

# Create the representation for the solution
# TODO: replace the below commands with your own commands
# to create the representation.txt, representation.json and
# mapping.json files
echo '' > "${representation_txt_file}"
echo '{"version": 1}' > "${representation_json_file}"
echo '{}' > "${mapping_file}"

# TODO(FAP): Load compiled files instead? For which platform do we have to do nativecomp?
emacs -batch -L "${representer_dir}" --init-directory="$representer_dir" -l "${representer_dir}/init.el" -l "${representer_dir}/representer.el" --eval "(exercism/represent \"${slug}\" \"${input_dir}\" \"${output_dir}\")"
# TODO(FAP): use jq to pretty print and order the json output instead of slow elisp?

echo "${slug}: done"
13 changes: 13 additions & 0 deletions src/init.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
(require 'package)
(package-initialize)
(unless package-archive-contents
(add-to-list
'package-archives '("gnu" . "https://elpa.gnu.org/packages/")
t)
(add-to-list
'package-archives '("melpa" . "https://melpa.org/packages/")
t)
(package-refresh-contents))
(dolist (pkg '(treepy))
(unless (package-installed-p pkg)
(package-install pkg)))
224 changes: 224 additions & 0 deletions src/representer.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
;;; representer.el --- Exercism Emacs Lisp Representer -*- lexical-binding: t; -*-

;; Package-Requires: ((emacs "29.4") (treepy "20230715.2154"))

;;; Commentary:

;;; Code:

(require 'cl-lib)
(require 'seq)
(require 'treepy)
;; TODO(FAP): recreate symbols with libs loaded: (mode-local cl-lib eieio seq subr subr-x)
;; Probably should load all built-in packages before?!
;; TODO(FAP): check which packages are activated when starting without config via 'package-activated-list var
;; TODO(FAP): is missing (define-package
(require 'symbols-from-obarray)
(require 'pp)

;; TODO(FAP): add debugging output for performance tests, timestamp after each phase

(defun exercism/represent (exercise-slug input-dir output-dir)
(let* ((timestamp (current-time))
(expressions-symbols-replaced
(exercism//represent input-dir exercise-slug timestamp)))
(with-temp-file (file-name-concat output-dir "mapping.json")
(insert (json-encode (exercism//placeholders->alist)))
(json-pretty-print-buffer-ordered)
(goto-char (point-max))
(newline))
(exercism//print-time-elapsed-since nil timestamp)
(with-temp-file (file-name-concat output-dir "representation.txt")
;; (pp-emacs-lisp-code expressions-symbols-replaced)
(insert (prin1-to-string expressions-symbols-replaced))
;; why does this indent differently than inside of Emacs with my config?!
(pp-buffer))
(exercism//print-time-elapsed-since nil timestamp)
(with-temp-file (file-name-concat output-dir
"representation.json")
(insert (json-encode '(("version" . 1))))
(json-pretty-print-buffer)
(goto-char (point-max))
(newline))
(exercism//print-time-elapsed-since nil timestamp)))

(defun exercism//represent (input-dir exercise-slug timestamp)
(let ((symbols-not-to-replace
(exercism//find-all-defined-symbols
(file-name-concat input-dir
(concat exercise-slug "-test.el")))))
(thread-first
(exercism//file-to-string
(file-name-concat input-dir (concat exercise-slug ".el")))
(exercism//print-time-elapsed-since timestamp)
(exercism//read-all-from-string)
(exercism//print-time-elapsed-since timestamp)
(exercism//macroexpand-all)
(exercism//print-time-elapsed-since timestamp)
(exercism//remove-docstrings)
(exercism//print-time-elapsed-since timestamp)
;; (exercism//replace-symbols-with-placeholders
;; symbols-not-to-replace)
(exercism//print-time-elapsed-since timestamp))))

(defun exercism//print-time-elapsed-since (returned timestamp)
(print
(concat
"### Time elapsed: "
(format-time-string "%s"
(time-subtract (current-time) timestamp))))
returned)

(defun exercism//file-to-string (file)
"Convert FILE to string."
(with-temp-buffer
(insert-file-contents file)
(buffer-string)))

(defun exercism//read-all-from-string (string)
(let* ((result '())
current-final-index)
(condition-case _error
(while t
(cl-destructuring-bind (object-read . final-string-index)
(read-from-string string current-final-index)
(setq result (cons object-read result))
(setq current-final-index final-string-index))
result)
(end-of-file))
(nreverse result)))

;; (defun exercism//remove-docstrings (expressions)
;; expressions)

(defun exercism//remove-docstrings (expressions)
(treepy-prewalk
(lambda (ele)
(cond
((and (listp ele)
(length> ele 3)
(member
(car ele)
'(defvar defconst defvar-1
defvar-local
defvar-mode-local
defconst-1
defconst-mode-local))
(stringp (nth 3 ele)))
(exercism//remove-nth-element 3 ele))
;; check for ~#'(lambda ...)~, which [cl-]defun expands to
;; TODO(FAP): position 2 is only a docstring if we have more than 3 elements
((and (listp ele)
(length> ele 1)
(eq (car ele) 'function)
(listp (nth 1 ele))
(length> (nth 1 ele) 3))
(let* ((lambda-expr (nth 1 ele))
(doc-string (nth 2 lambda-expr)))
(if (stringp doc-string)
(exercism//remove-nth-element 2 (nth 1 ele))))
ele)
((and (listp ele)
(length> ele 3)
(member
(car ele)
'(defalias defvaralias make-obsolete-variable
autoload
define-abbrev-table
define-package
iter-defun)))
(exercism//remove-nth-element 3 ele))
((and (listp ele)
(length> ele 2)
(member (car ele) '(define-category)))
(exercism//remove-nth-element 2 ele))
(t
ele)))
expressions))

(defun exercism//macroexpand-all (expressions)
(mapcar
(lambda (expression) (macroexpand-all expression)) expressions))

(defun exercism//find-all-defined-symbols (test-file)
"Find all symbols defined in the current Emacs environment
and symbols from the test file."
(let ((test-file-expressions
(exercism//read-all-from-string
(exercism//file-to-string test-file)))
(symbols (exercism//symbols-from-obarray)))
(treepy-prewalk
(lambda (ele)
(cond
((and (listp ele) (eq 'declare-function (car ele)))
nil)
((symbolp ele)
(puthash ele t symbols))
(t
ele)))
test-file-expressions)
symbols))

(defun exercism//replace-symbols-with-placeholders
(expressions symbols-not-to-replace)
(treepy-prewalk
(lambda (ele)
(cond
((and (symbolp ele)
(or (exercism//symbol-is-keyword-p ele)
(gethash ele symbols-not-to-replace)))
ele)
((and (symbolp ele))
(exercism//add-placeholder ele))
(t
ele)))
expressions))

(defun exercism//remove-nth-element (nth list)
(if (zerop nth)
(cdr list)
(let ((last (nthcdr (1- nth) list)))
(setcdr last (cddr last))
list)))

(defun exercism//symbol-is-keyword-p (symbol)
(eq (aref (symbol-name symbol) 0) ?&))


(defvar exercism//placeholders '())
(defvar exercism//counter 0)

;; TODO(FAP): why do we have duplicates in the placeholders? --cl-rest-- and --cl-keys--
;; how does rassoc check equality?
(defun exercism//find-placeholder (symbol)
(car (rassoc symbol exercism//placeholders)))

(defun exercism//find-original-symbol (placeholder)
(assoc placeholder exercism//placeholders))

(defun exercism//new-placeholder ()
(prog1 (intern (format "PLACEHOLDER-%d" exercism//counter))
(cl-incf exercism//counter)))

(defun exercism//add-placeholder (symbol)
(if (and symbol (symbolp symbol))
(let ((existing (exercism//find-placeholder symbol)))
(or existing
(let ((new-symbol (exercism//new-placeholder)))
(setf exercism//placeholders
(cl-acons
new-symbol symbol exercism//placeholders))
new-symbol)))
symbol))

(defun exercism//placeholders->alist ()
(mapcar
#'(lambda (acons)
(cons
(prin1-to-string (car acons) t)
(prin1-to-string (cdr acons) t)))
exercism//placeholders))


(provide 'representer)
;;; representer.el ends here
8 changes: 8 additions & 0 deletions src/symbols-from-obarray.el

Large diffs are not rendered by default.

Loading
Loading