diff --git a/.ccls b/.ccls deleted file mode 100644 index cd50a2e..0000000 --- a/.ccls +++ /dev/null @@ -1,7 +0,0 @@ -clang -%c -std=c11 -%cpp -std=c++17 --Igodot-cpp/include/ --Igodot-cpp/gdextension/ --Igodot-cpp/gen/include/ --Is7/ diff --git a/.gitignore b/.gitignore index aa6a744..793f852 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,15 @@ custom.py # Ignore generated compile_commands.json compile_commands.json +.ccls +.ccls-cache/ # Ignore files generated for documentation /src/gen +# Ignore files generated for the repl +/src/repl/gen + # Binaries *.o *.os diff --git a/Makefile b/Makefile index ea186de..5e4587b 100644 --- a/Makefile +++ b/Makefile @@ -8,20 +8,19 @@ bin/s7: s7/s7.c @echo ⚙️ Building scheme interpreter gcc s7/s7.c -o bin/s7 -DWITH_MAIN -DWITH_SYSTEM_EXTRAS -DWITH_C_LOADER=0 -I. -O2 -g -ldl -lm -demo/.godot: $(wildcard demo/addons/**) $(wildcard demo/bin/**) build - @echo 📦 Importing test scene - godot --path demo --headless --import - -.PHONY: build +SRC_FILES := $(shell find src -type f ! -name "*.os") +DEMO_FILES := $(shell find demo -type f -name "*.tscn" -or -name "*.scm") -build: +demo/.godot: $(SRC_FILES) $(DEMO_FILES) @echo ⚙️ Building extension @scons + @echo 📦 Importing test scene + godot --path demo --headless --import .PHONY: run -run: - scons && godot -e --path demo main.tscn +run: demo/.godot + godot -e --path demo main.tscn .PHONY: android diff --git a/README.md b/README.md index 665bac2..11a4c2b 100644 --- a/README.md +++ b/README.md @@ -20,24 +20,24 @@ A more complete example of the available syntax: (define button (*node* 'owner '(get_child 1))) (define (button-append! suffix) - (let ((text (button 'text))) - ;; Godot properties are set via generalized set! syntax - ;; and there are two main ways of calling Godot methods: - ;; * (! &) - ;; * ( '( &)) - ;; ! is preferred for effectful calls such as - ;; 'insert below, and, in general is more amenable - ;; to optimisations. Applicable object syntax - ;; is convenient for const methods like '(length) below and - ;; `(get_child 1) above. - (set! (button 'text) - (! text 'insert (text '(length)) suffix)))) + (let ((text (button 'text))) + ;; Godot properties are set via generalized set! syntax + ;; and there are two main ways of calling Godot methods: + ;; * (! &) + ;; * ( '( &)) + ;; ! is preferred for effectful calls such as + ;; 'insert below, and, in general is more amenable + ;; to optimisations. Applicable object syntax + ;; is convenient for const methods like '(length) below and + ;; `(get_child 1) above. + (set! (button 'text) + (! text 'insert (text '(length)) suffix)))) (define (function-handler) - (button-append! "!")) + (button-append! "!")) (define (symbol-handler) - (button-append! "'")) + (button-append! "'")) ;; Signals can be connected to symbols, lambdas and arbitrary procedures. ;; Symbols provide late binding, i.e., the ability to redefine the @@ -57,19 +57,19 @@ Very experimental but a lot of fun to play with. Use it at your own risk. Make sure to update all git submodules: ```shell - git submodule update --init + git submodule update --init ``` -Build and launch the demo project with: +Build and launch the demo project with `make run` or more explicitly via: ```shell - scons && godot -e --path demo + scons && godot -e --path demo ``` -Build the Android target with: +Build the Android target with `make android` or more explicitly via: ```shell - scons platform=android target=template_debug + scons platform=android target=template_debug ``` Make sure `ANDROID_HOME` is set. @@ -79,22 +79,29 @@ Make sure `ANDROID_HOME` is set. Install [Geiser](https://www.nongnu.org/geiser/) then add the following to your Emacs configuration: ```elisp - (add-to-list 'load-path "~/path/to/godot-s7-scheme/emacs/") - (load "geiser-godot-s7-autoloads.el") +(add-to-list 'load-path "~/path/to/godot-s7-scheme/emacs/") +(load "geiser-godot-s7-autoloads.el") ``` The Emacs extension automatically recognize Scheme files inside Godot project directories as `Godot s7 Scheme` files. -### Connecting +### Connecting to the editor -1. Add a `SchemeReplServer` to your scene (preferably as a child of a `Scheme` node) and set its `Auto Start` property to `true`. -2. Check the port number in the Godot output window. -3. `M-x connect-to-godot-s7` +1. Start Godot with `--s7-tcp-port=` (and/or `--s7-tcp-address=`). +2. Check the port number in the console. +3. In Emacs, `M-x connect-to-godot-s7` +3.1 You can also open a Scheme repl from the shell with `nc ` + +### Connecting to a running scene + +1. In Godot, select `Debug / Customize Run Instances... / Main Run Args` + - Add `--s7-tcp-port=` and/or `--s7-tcp-address=`. +2. Steps 2 and 3 as above. ## Roadmap - [x] use Godot API from Scheme -- [o] live coding interface via Emacs (wip) +- [x] live coding interface via Emacs - [ ] expose tree-sitter API to Scheme - [ ] Scheme editor with syntax highlighting - [ ] Scheme notebooks diff --git a/SConstruct b/SConstruct index d35dc17..4a428da 100644 --- a/SConstruct +++ b/SConstruct @@ -1,39 +1,62 @@ #!/usr/bin/env python import os import sys - from methods import print_error +# -------------------------- Utility Functions -------------------------- +def embed_file(target, source, env): + """ + Embeds the content of file into a header file as a const char* constant. + """ + target_path = str(target[0]) + source_path = str(source[0]) + + # Read the content of the source file + with open(source_path, 'r') as src_file: + file_content = src_file.read() + + # Generate a unique header guard based on the file name + target_file_name = os.path.basename(target_path) + header_guard = target_file_name.replace('-', '_').replace('.', '_').upper() + + # Write the output header file + constant_name = os.path.splitext(target_file_name)[0].replace('-', '_') + with open(target_path, 'w') as target_file: + target_file.write(f""" +#ifndef {header_guard} +#define {header_guard} + +const char* {constant_name} = R"({file_content})"; + +#endif // {header_guard} +""") -libname = "godot-s7-scheme" -projectdir = "demo" +def is_submodule_initialized(path): + return os.path.isdir(path) and os.listdir(path) -localEnv = Environment(tools=["default"], PLATFORM="") +# -------------------------- Build definition -------------------------- + +lib_name = "godot-s7-scheme" +project_dir = "demo" + +local_env = Environment(tools=["default"], PLATFORM="") customs = ["custom.py"] customs = [os.path.abspath(path) for path in customs] opts = Variables(customs, ARGUMENTS) -opts.Update(localEnv) - -Help(opts.GenerateHelpText(localEnv)) +opts.Update(local_env) -env = localEnv.Clone() +Help(opts.GenerateHelpText(local_env)) -submodule_initialized = False -dir_name = 'godot-cpp' -if os.path.isdir(dir_name): - if os.listdir(dir_name): - submodule_initialized = True - -if not submodule_initialized: +if not is_submodule_initialized('godot-cpp'): print_error("""godot-cpp is not available within this folder, as Git submodules haven't been initialized. Run the following command to download godot-cpp: git submodule update --init --recursive""") sys.exit(1) -env = SConscript("godot-cpp/SConstruct", {"env": env, "customs": customs}) +env = SConscript("godot-cpp/SConstruct", {"env": local_env.Clone(), "customs": customs}) env.Append( CPPPATH=["src/", "s7/"], CPPDEFINES={ @@ -48,6 +71,7 @@ env.Append( sources = [ Glob("src/*.cpp"), + Glob("src/repl/*.cpp"), Glob("s7/s7.c") ] @@ -58,20 +82,26 @@ if env["target"] in ["editor", "template_debug"]: except AttributeError: print("Not including class reference as we're targeting a pre-4.3 baseline.") -file = "{}{}{}".format(libname, env["suffix"], env["SHLIBSUFFIX"]) -filepath = "" +file = "{}{}{}".format(lib_name, env["suffix"], env["SHLIBSUFFIX"]) +file_path = "" if env["platform"] == "macos" or env["platform"] == "ios": - filepath = "{}.framework/".format(env["platform"]) - file = "{}.{}.{}".format(libname, env["platform"], env["target"]) + file_path = "{}.framework/".format(env["platform"]) + file = "{}.{}.{}".format(lib_name, env["platform"], env["target"]) -libraryfile = "bin/{}/{}{}".format(env["platform"], filepath, file) +library_file = "bin/{}/{}{}".format(env["platform"], file_path, file) library = env.SharedLibrary( - libraryfile, + library_file, source=sources, ) -copy = env.InstallAs("{}/bin/{}/{}lib{}".format(projectdir, env["platform"], filepath, file), library) +copy = env.InstallAs("{}/bin/{}/{}lib{}".format(project_dir, env["platform"], file_path, file), library) + +embed_scheme_repl = env.Command( + target="src/repl/gen/s7_scheme_repl_string.hpp", + source="demo/addons/s7/s7_scheme_repl.scm", + action=embed_file +) -default_args = [library, copy] +default_args = [embed_scheme_repl, library, copy] Default(*default_args) diff --git a/demo/project.godot b/demo/project.godot index 2a5ec61..6d32385 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -10,6 +10,6 @@ config_version=5 [application] -config/name="godot cpp template" +config/name="godot s7 scheme" config/features=PackedStringArray("4.3", "Forward Plus") config/icon="res://icon.svg" diff --git a/emacs/.gitignore b/emacs/.gitignore new file mode 100644 index 0000000..c531d98 --- /dev/null +++ b/emacs/.gitignore @@ -0,0 +1 @@ +*.elc diff --git a/emacs/geiser-godot-s7-autoloads.el b/emacs/geiser-godot-s7-autoloads.el new file mode 100644 index 0000000..264b3eb --- /dev/null +++ b/emacs/geiser-godot-s7-autoloads.el @@ -0,0 +1,42 @@ +;;; geiser-godot-s7-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + + +;;;### (autoloads nil "geiser-godot-s7" "geiser-godot-s7.el" (0 0 +;;;;;; 0 0)) +;;; Generated autoloads from geiser-godot-s7.el + +(autoload 'connect-to-godot-s7 "geiser-godot-s7" "\ +Start a Godot s7 REPL connected to a remote process. + +Start a Scheme Repl in the active Godot s7 scene." t) + +(geiser-activate-implementation 'godot-s7) + +(autoload 'run-godot-s7 "geiser-godot-s7" "\ +Start a Geiser Godot s7 REPL." t) + +(autoload 'switch-to-godot-s7 "geiser-godot-s7" "\ +Start a Geiser Godot s7 REPL, or switch to a running one." t) + +(register-definition-prefixes "geiser-godot-s7" '("geiser-godot-s7-" "godot-s7")) + +;;;*** + +;;; End of scraped data + +(provide 'geiser-godot-s7-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; geiser-godot-s7-autoloads.el ends here diff --git a/emacs/geiser-godot-s7.el b/emacs/geiser-godot-s7.el new file mode 100644 index 0000000..703a51a --- /dev/null +++ b/emacs/geiser-godot-s7.el @@ -0,0 +1,359 @@ +;;; geiser-godot-s7.el --- Godot s7 and Geiser talk to each other -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Rodrigo B. de Oliveira +;; Start date: Feb, 2024 + +;; Author: Rodrigo B. de Oliveira (rbo@acm.org) +;; Maintainer: Rodrigo B. de Oliveira (rbo@acm.org) +;; Keywords: languages, godot, s7, scheme, geiser +;; Homepage: https://github.com/bamboo/godot-s7-scheme +;; Package-Requires: ((emacs "25.1") (geiser "0.28.1")) +;; SPDX-License-Identifier: BSD-3-Clause +;; Version: 0.1.0 + +;; This file is NOT part of GNU Emacs. + +;;; Commentary: + +;; This package extends the `geiser' core package to support Godot s7. + + +;;; Code: + +(require 'geiser-connection) +(require 'geiser-syntax) +(require 'geiser-custom) +(require 'geiser-repl) +(require 'geiser-debug) +(require 'geiser-impl) +(require 'geiser-base) +(require 'geiser-eval) +(require 'geiser-edit) +(require 'geiser-log) +(require 'geiser) + +(require 'compile) +(require 'info-look) + +(eval-when-compile + (require 'cl-lib) + (require 'subr-x)) + + +;;; Customization + +(defgroup geiser-godot-s7 nil + "Customization for Geiser's Godot s7 flavour." + :group 'geiser) + +(geiser-custom--defcustom geiser-godot-s7-binary "godot" + "Name to use to call the Godot executable when starting a REPL." + :type '(choice string (repeat string))) + +(geiser-custom--defcustom geiser-godot-s7-load-path nil + "A list of paths to be added to Godot s7's load path when it's started. +The paths are added to both %`load-path' and %load-compiled path, +and only if they are not already present. This variable is a +good candidate for an entry in your project's .dir-locals.el." + :type '(repeat file)) + +(geiser-custom--defcustom geiser-godot-s7-init-file "~/.godot-s7" + "Initialization file with user code for the Godot s7 REPL." + :type 'string) + +(geiser-custom--defcustom geiser-godot-s7-extra-keywords nil + "Extra keywords highlighted in Godot s7 scheme buffers." + :type '(repeat string)) + +(geiser-custom--defcustom geiser-godot-s7-manual-lookup-other-window nil + "Non-nil means pop up the Info buffer in another window." + :type 'boolean) + +(geiser-custom--defcustom geiser-godot-s7-manual-lookup-nodes + '("godotengine") + "List of info nodes that, when present, are used for manual lookups." + :type '(repeat string)) + + +;;; REPL support + +(defun geiser-godot-s7--binary () + "Return the name of the Godot s7 binary to execute." + (if (listp geiser-godot-s7-binary) + (car geiser-godot-s7-binary) + geiser-godot-s7-binary)) + +(defvar geiser-godot-s7--conn-address "9998") + +(defun geiser-godot-s7--parameters () + "Return a list with all parameters needed to start Godot s7." + '("godot")) + +(defconst geiser-godot-s7--prompt-regexp "^s7@([^)]*)> ") + + +;;; Evaluation support +(defsubst geiser-godot-s7--linearize-args (args) + "Concatenate the list ARGS." + (mapconcat 'identity args " ")) + +(defun geiser-godot-s7--geiser-procedure (proc &rest args) + "Transform PROC in string for a scheme procedure using ARGS." + (cl-case proc + ((eval compile) (format ",geiser-eval %s %s%s" + (or (car args) "#f") + (geiser-godot-s7--linearize-args (cdr args)) + (if (cddr args) "" " ()"))) + ((load-file compile-file) (format ",geiser-load-file %s" (car args))) + ((no-values) ",geiser-no-values") + (t (format "ge:%s (%s)" proc (geiser-godot-s7--linearize-args args))))) + +(defun geiser-godot-s7--clean-up-output (str) + str) + +(defun geiser-godot-s7--get-module (&optional _module) + "Find current buffer's module using MODULE as a hint." + :f) + +(defun geiser-godot-s7--module-cmd (module fmt &optional def) + "Use FMT to format a change to MODULE, with default DEF." + (when module + (let* ((module (geiser-godot-s7--get-module module)) + (module (cond ((or (null module) (eq module :f)) def) + (t (format "%s" module))))) + (and module (format fmt module))))) + +(defun geiser-godot-s7--import-command (module) + "Format a REPL command to use MODULE." + (geiser-godot-s7--module-cmd module ",use %s")) + +(defun geiser-godot-s7--enter-command (module) + "Format a REPL command to enter MODULE." + (geiser-godot-s7--module-cmd module ",m %s" "(godot-s7)")) + +(defun geiser-godot-s7--exit-command () + "Format a REPL command to quit." + ",q") + +(defun geiser-godot-s7--symbol-begin (module) + "Find beginning of symbol in the context of MODULE." + (if module + (max (save-excursion (beginning-of-line) (point)) + (save-excursion (skip-syntax-backward "^(>") (1- (point)))) + (save-excursion (skip-syntax-backward "^'-()>") (point)))) + + +;;; Compilation shell regexps + +(defconst geiser-godot-s7--path-rx "^In \\([^:\n ]+\\):\n") + +(defconst geiser-godot-s7--rel-path-rx "^In +\\([^/\n: ]+\\):\n") + +(defvar geiser-godot-s7--file-cache (make-hash-table :test 'equal) + "Internal cache.") + +(defun geiser-godot-s7--find-file (file) + (or (gethash file geiser-godot-s7--file-cache) + (with-current-buffer (or geiser-debug--sender-buffer (current-buffer)) + (when-let (r geiser-repl--repl) + (with-current-buffer r + (geiser-eval--send/result `(:eval (:ge find-file ,file)))))))) + +(defun geiser-godot-s7--resolve-file (file) + "Find the given FILE, if it's indeed a file." + (when (and (stringp file) + (not (member file + '("socket" "stdin" "unknown file" "current input")))) + (message "Resolving %s" file) + (cond ((file-name-absolute-p file) file) + (t (when-let (f (geiser-godot-s7--find-file file)) + (puthash file f geiser-godot-s7--file-cache)))))) + +(defun geiser-godot-s7--resolve-file-x () + "Check if last match contain a resolvable file." + (let ((f (geiser-godot-s7--resolve-file (match-string-no-properties 1)))) + (and (stringp f) (list f)))) + + +;;; Error display and debugger + +(defun geiser-godot-s7--set-up-error-links () + (setq-local compilation-error-regexp-alist + `((,geiser-godot-s7--path-rx geiser-godot-s7--resolve-file-x) + ("^ +\\([0-9]+\\):\\([0-9]+\\)" nil 1 2) + ("^\\(/.*\\):\\([0-9]+\\):\\([0-9]+\\)" 1 2 3))) + (font-lock-add-keywords nil + `((,geiser-godot-s7--path-rx 1 compilation-error-face)))) + +(defun geiser-godot-s7--display-error (_module _key msg) + "Display error with given message MSG." + (when (stringp msg) + (geiser-godot-s7--set-up-error-links) + (save-excursion (insert msg))) + (not (zerop (length msg)))) + + +;;; Trying to ascertain whether a buffer is Godot s7 Scheme + +(defun geiser-godot-s7--guess () + "Ascertain whether the file belongs to a Godot project." + (locate-dominating-file (buffer-file-name) "project.godot")) + + +;;; Keywords and syntax + +(defconst geiser-godot-s7--builtin-keywords + '("call-with-input-file" + "call-with-input-string" + "call-with-output-file" + "call-with-output-string" + "with-output-to-string" + "define*" + "define-macro*" + "define-bacro" + "define-bacro*" + "define-constant" + "lambda*" + "set!" + "call!" + "inc!" + "connect!" + "disconnect!" + "$" + "require" + "provide" + "import" + "import-class" + "define-signal" + )) + +(defun geiser-godot-s7--keywords () + "Return Godot s7-specific scheme keywords." + (append + (geiser-syntax--simple-keywords geiser-godot-s7-extra-keywords) + (geiser-syntax--simple-keywords geiser-godot-s7--builtin-keywords))) + +(geiser-syntax--scheme-indent + (call-with-input-string 1) + (call-with-output-string 0) + (call-with-exit 0) + (define* 1) + (define-macro* 1) + (define-bacro 1) + (define-bacro* 1) + (lambda* 1) + (doto 1) + (with-let 1) + (when-let 1) + (with-output-to-string 0)) + + +;;; REPL startup + +(defconst geiser-godot-s7-minimum-version "0.1") + +(defun geiser-godot-s7--version (_binary) + "Find Godot s7's version running the configured Godot s7 binary." + geiser-godot-s7-minimum-version) + +;;;###autoload +(defun connect-to-godot-s7 () + "Start a Godot s7 REPL connected to a remote process. + +Start a Scheme Repl in the active Godot s7 scene." + (interactive) + (geiser-connect 'godot-s7)) + +(defun geiser-godot-s7--startup (_remote) + "Startup function, for a remote connection if REMOTE is t." + (geiser-godot-s7--set-up-error-links)) + + +;;; Manual lookup + +(defun geiser-godot-s7--info-spec () + "Return info specification for given NODES." + (let* ((nrx "^[ ]+-+ [^:]+:[ ]*") + (drx "\\b") + (res (when (Info-find-file "r5rs" t) + `(("(r5rs)Index" nil ,nrx ,drx))))) + (dolist (node geiser-godot-s7-manual-lookup-nodes res) + (when (Info-find-file node t) + (mapc (lambda (idx) + (add-to-list 'res + (list (format "(%s)%s" node idx) nil nrx drx))) + '("R5RS Index" "Concept Index" "Procedure Index" "Variable Index", "Index")))))) + +(info-lookup-add-help :topic 'symbol + :mode 'geiser-godot-s7-mode + :ignore-case nil + :regexp "[^()`',\" \n]+" + :doc-spec (geiser-godot-s7--info-spec)) + +(defun geiser-godot-s7--info-lookup (id) + (cond ((null id) (info "godotengine")) + ((ignore-errors (info-lookup-symbol (format "%s" id) 'geiser-godot-s7-mode) t)) + ((and (listp id) (geiser-godot-s7--info-lookup (car (last id))))) + (t (geiser-godot-s7--info-lookup (when (listp id) (butlast id)))))) + +(defun geiser-godot-s7--manual-look-up (id _mod) + "Look for ID in the Godot s7 manuals." + (let ((info-lookup-other-window-flag geiser-godot-s7-manual-lookup-other-window)) + (geiser-godot-s7--info-lookup id) + (when geiser-godot-s7-manual-lookup-other-window + (switch-to-buffer-other-window "*info*")))) + + +;;; debugging +(when nil + (trace-function-foreground 'geiser-godot-s7--binary) + (trace-function-foreground 'geiser-godot-s7--parameters) + (trace-function-foreground 'geiser-godot-s7--version) + (trace-function-foreground 'geiser-godot-s7--startup) + (trace-function-foreground 'geiser-godot-s7--clean-up-output) + (trace-function-foreground 'geiser-godot-s7--geiser-procedure) + (trace-function-foreground 'geiser-godot-s7--guess) + (trace-function-foreground 'geiser-godot-s7--symbol-begin) + (trace-function-foreground 'geiser-godot-s7--enter-command) + (trace-function-foreground 'geiser-godot-s7--exit-command) + (trace-function-foreground 'geiser-godot-s7--get-module) + (trace-function-foreground 'geiser-godot-s7--keywords) + (trace-function-foreground 'geiser-godot-s7--display-error)) + +;;; Implementation definition: + +(define-geiser-implementation godot-s7 + (binary geiser-godot-s7--binary) + (arglist geiser-godot-s7--parameters) + (version-command geiser-godot-s7--version) + (minimum-version geiser-godot-s7-minimum-version) + (repl-startup geiser-godot-s7--startup) + (prompt-regexp geiser-godot-s7--prompt-regexp) + (clean-up-output geiser-godot-s7--clean-up-output) + (debugger-prompt-regexp nil) + (enter-debugger nil) + (marshall-procedure geiser-godot-s7--geiser-procedure) + (find-module geiser-godot-s7--get-module) + (enter-command geiser-godot-s7--enter-command) + (exit-command geiser-godot-s7--exit-command) + (import-command geiser-godot-s7--import-command) + (find-symbol-begin geiser-godot-s7--symbol-begin) + (display-error geiser-godot-s7--display-error) + (external-help nil) + (check-buffer geiser-godot-s7--guess) + (keywords geiser-godot-s7--keywords) + (case-sensitive :t) + (unsupported '(callers callees))) + +;;;###autoload +(geiser-activate-implementation 'godot-s7) + +;;;###autoload +(autoload 'run-godot-s7 "geiser-godot-s7" "Start a Geiser Godot s7 REPL." t) + +;;;###autoload +(autoload 'switch-to-godot-s7 "geiser-godot-s7" + "Start a Geiser Godot s7 REPL, or switch to a running one." t) + +(provide 'geiser-godot-s7) +;;; geiser-godot-s7.el ends here diff --git a/src/debug_macros.hpp b/src/debug_macros.hpp index 84df2da..0950ce1 100644 --- a/src/debug_macros.hpp +++ b/src/debug_macros.hpp @@ -14,9 +14,9 @@ #define LOG_CALL() (std::cout << __func__ << ":" << __LINE__ << std::endl) s7_pointer watch_s7_value( - s7_scheme *sc, const char *func, int line, const char *e, s7_pointer v); + s7_scheme *sc, const char *func, int line, const char *e, s7_pointer v); const godot::Variant &watch_variant( - const char *func, int line, const char *e, const godot::Variant &v); + const char *func, int line, const char *e, const godot::Variant &v); #else #define WATCH(e) 0 diff --git a/src/ffi.hpp b/src/ffi.hpp index c6bb899..1fe4f52 100644 --- a/src/ffi.hpp +++ b/src/ffi.hpp @@ -1,7 +1,7 @@ #ifndef GODOT_S7_SCHEME_FFI_H #define GODOT_S7_SCHEME_FFI_H -#include +#include "s7.hpp" namespace godot { void define_variant_ffi(s7 &s7); @@ -35,4 +35,4 @@ inline auto scheme_object_to_godot_string(s7_scheme *sc, s7_pointer o) { } } //namespace godot -#endif //GODOT_S7_SCHEME_FFI_H \ No newline at end of file +#endif //GODOT_S7_SCHEME_FFI_H diff --git a/src/register_types.cpp b/src/register_types.cpp index 9391e85..985fa9a 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,9 +1,11 @@ #include "register_types.h" #include "scheme.hpp" #include "scheme_object.hpp" +#include "scheme_repl_server.hpp" #include "scheme_script.hpp" #include "scheme_script_loader.hpp" #include +#include #include #include #include @@ -13,10 +15,7 @@ using namespace godot; static Ref script_loader; -void initialize_gdextension_types(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { - return; - } +void initialize_scene_types() { GDREGISTER_CLASS(SchemeScript); GDREGISTER_CLASS(SchemeScriptLoader); GDREGISTER_CLASS(Scheme); @@ -26,15 +25,50 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) { ResourceLoader::get_singleton()->add_resource_format_loader(script_loader); } -void uninitialize_gdextension_types(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { - return; - } - +void uninitialize_scene_types() { ResourceLoader::get_singleton()->remove_resource_format_loader(script_loader); script_loader.unref(); } +static SchemeReplServer *repl_server; + +void initialize_server_types() { + GDREGISTER_CLASS(SchemeReplServer); + repl_server = memnew(SchemeReplServer); + repl_server->start(); +} + +void uninitialize_server_types() { + repl_server->stop(); + memdelete(repl_server); +} + +void initialize_gdextension_types(ModuleInitializationLevel p_level) { + switch (p_level) { + case MODULE_INITIALIZATION_LEVEL_SCENE: + initialize_scene_types(); + break; + case MODULE_INITIALIZATION_LEVEL_EDITOR: + initialize_server_types(); + break; + default: + break; + } +} + +void uninitialize_gdextension_types(ModuleInitializationLevel p_level) { + switch (p_level) { + case MODULE_INITIALIZATION_LEVEL_SCENE: + uninitialize_scene_types(); + break; + case MODULE_INITIALIZATION_LEVEL_EDITOR: + uninitialize_server_types(); + break; + default: + break; + } +} + extern "C" { GDExtensionBool GDE_EXPORT godot_s7_scheme_library_init( GDExtensionInterfaceGetProcAddress p_get_proc_address, diff --git a/src/repl/connection.cpp b/src/repl/connection.cpp new file mode 100644 index 0000000..4b460e5 --- /dev/null +++ b/src/repl/connection.cpp @@ -0,0 +1,154 @@ +#include "connection.hpp" +#include "debug.hpp" +#include + +using namespace godot; + +void ReplConnection::disconnect() { + tcp_stream->disconnect_from_host(); +} + +String ReplConnection::get_prompt() { + if (target_node) { + return "\ns7@(" + target_node->node_name + ")> "; + } + return "\ns7@(:)> "; +} + +void ReplConnection::send(char c) { + tcp_stream->put_8(c); +} + +void ReplConnection::send(const char *p, size_t count) { + for (int i = 0; i < count; ++i) { + send(p[i]); + } +} + +void ReplConnection::send(const String &s) { + auto utf8 = s.utf8(); + send(utf8.get_data(), utf8.length()); +} + +void ReplConnection::send_prompt() { + send(get_prompt()); +} + +void ReplConnection::on_eval_async_result(const String &s) { + send(s); + send_prompt(); +} + +ReplConnection::Status ReplConnection::process_with(Context &context) { + if (tcp_stream->get_status() != StreamPeerTCP::STATUS_CONNECTED) { + return ReplConnection::DISCONNECTED; + } + + auto available = tcp_stream->get_available_bytes(); + auto originally_available = available; + while (available > 0) { + available--; + auto ch = tcp_stream->get_8(); + +#if DEBUG_REPL_INTERACTIONS + putchar(ch); +#endif + + if (ch == '\n' && available == 0) { + if (!process_buffer_with(context)) { + return ReplConnection::DISCONNECTED; + } + buffer.clear(); + } else { + buffer.push_back(ch); + } + } + if (tcp_stream->poll() != Error::OK) { + return ReplConnection::DISCONNECTED; + } + return originally_available > 0 + ? ReplConnection::TRANSMITTING + : ReplConnection::IDLE; +} + +template +bool buffer_contains(const char (&str)[N], B buffer) { + constexpr size_t strlen = N - 1; + return buffer.size() == strlen && std::strncmp((const char *)buffer.ptr(), str, strlen) == 0; +} + +template +bool buffer_starts_with(const char (&str)[N], B buffer) { + constexpr size_t strlen = N - 1; + return buffer.size() >= strlen && std::strncmp((const char *)buffer.ptr(), str, strlen) == 0; +} + +bool ReplConnection::process_buffer_with(Context &context) { + if (buffer_contains(",q", buffer)) { + // disconnection from repl + return false; + } + if (buffer_contains(",ls", buffer)) { + send_available_nodes(); + return true; + } + if (buffer_starts_with(",enter ", buffer)) { + enter_selected_node(buffer.get_string_from_utf8().substr(7)); + return true; + } + + process_eval_request_with(context); + return true; +} + +void ReplConnection::send_available_nodes() { + auto sent = 0; + for (const auto &node_name : node_registry->get_available_node_names()) { + if (sent++ > 0) { + send('\n'); + } + send(node_name); + } + send_prompt(); +} + +void ReplConnection::enter_selected_node(const String &node_name) { + auto found = node_registry->find_node_by_name(node_name); + if (found) { + target_node = found; + send("Connected to `" + node_name + "`."); + } else { + send("Node `" + node_name + "` not found."); + } + send_prompt(); +} + +void ReplConnection::send_output(const String &output) { + if (output.is_empty()) + return; + send(output); + send('\n'); +} + +void ReplConnection::process_eval_request_with(Context &context) { + auto [output, code] = context.compiler().compile_request(buffer); + send_output(output); + + if (!code.is_empty()) { + if (target_node) { + if (context.eval_async(code, target_node->node_id)) { + // TODO: enter waiting state + return; + } else { + send("Target scheme node has been disconnected"); + target_node = std::nullopt; + } + } else { + auto result = context.compiler().eval(code); + DEBUG_REPL(result); + send(result); + } + } + + send_prompt(); +} diff --git a/src/repl/connection.hpp b/src/repl/connection.hpp new file mode 100644 index 0000000..5abd15f --- /dev/null +++ b/src/repl/connection.hpp @@ -0,0 +1,55 @@ +#ifndef GODOT_S7_SCHEME_REPL_REQUEST_CONNECTION_HPP +#define GODOT_S7_SCHEME_REPL_REQUEST_CONNECTION_HPP + +#include "node_registry.hpp" +#include "request_compiler.hpp" +#include +#include +#include +#include +#include + +class ReplConnection { +public: + struct Context { + virtual ReplRequestCompiler &compiler() = 0; + virtual bool eval_async(const godot::String &compiled_request, uint64_t node_instance_id) = 0; + }; + + enum Status { + IDLE, + TRANSMITTING, + DISCONNECTED + }; + + ReplConnection( + godot::Ref tcp_stream, + std::shared_ptr node_registry) : + tcp_stream(tcp_stream), + node_registry(node_registry), + target_node(node_registry->get_most_recent()) {} + +public: + void send_prompt(); + Status process_with(Context &context); + void on_eval_async_result(const godot::String &s); + void disconnect(); + +private: + godot::String get_prompt(); + void send_output(const godot::String &s); + void send(const godot::String &s); + void send(const char *p, size_t count); + void send(char c); + bool process_buffer_with(Context &context); + void process_eval_request_with(Context &context); + void send_available_nodes(); + void enter_selected_node(const godot::String &node_name); + +private: + godot::Ref tcp_stream; + godot::PackedByteArray buffer; + std::shared_ptr node_registry; + std::optional target_node; +}; +#endif diff --git a/src/repl/debug.hpp b/src/repl/debug.hpp new file mode 100644 index 0000000..2d4a43b --- /dev/null +++ b/src/repl/debug.hpp @@ -0,0 +1,16 @@ +#ifndef GODOT_S7_SCHEME_REPL_DEBUG_HPP +#define GODOT_S7_SCHEME_REPL_DEBUG_HPP + +#include + +using gd = godot::UtilityFunctions; + +#define DEBUG_REPL_INTERACTIONS 0 + +#if DEBUG_REPL_INTERACTIONS +#define DEBUG_REPL(...) gd::print(__VA_ARGS__) +#else +#define DEBUG_REPL(...) 0 +#endif + +#endif diff --git a/src/repl/mediator.cpp b/src/repl/mediator.cpp new file mode 100644 index 0000000..3e2768b --- /dev/null +++ b/src/repl/mediator.cpp @@ -0,0 +1,72 @@ +#include "mediator.hpp" +#include "../scheme.hpp" +#include "debug.hpp" +#include + +using namespace godot; + +ReplMediator::ReplMediator(Ref server, Callable reply) : + server(server), eval_async_continuation(reply) { + node_registry = std::make_shared(); +} + +struct ReplMessageHandler { + ReplMediator *mediator; + + void operator()(const ReplMessage::PublishNode &m) { + mediator->node_registry->register_node(m.node_id, std::move(m.node_name)); + } + void operator()(const ReplMessage::UnpublishNode &m) { + mediator->node_registry->unregister_node(m.node_id); + } + void operator()(const ReplMessage::EvalResponse &m) { + for (auto &[connection, context] : mediator->connections) { + if (context.connection_id == m.connection_id) { + connection.on_eval_async_result(m.result); + break; + } + } + } +}; + +bool ReplMediator::ReplConnectionContext::eval_async(const String &code, uint64_t scheme_node_id) { + auto scheme_node = ObjectDB::get_instance(scheme_node_id); + if (!scheme_node) { + return false; + } + scheme_node->call_deferred( + "eval_async", + code, + mediator->eval_async_continuation.bind(connection_id)); + return true; +} + +bool ReplMediator::mediate(MessageQueue &queue) { + int interactions = 0; + auto message = queue.pop(); + if (message) { + std::visit(ReplMessageHandler{ this }, message->payload); + interactions++; + } + + if (server->is_connection_available()) { + auto connection = ReplConnection(server->take_connection(), node_registry); + gd::print("Scheme repl client connected."); + connection.send_prompt(); + connections.emplace_back(std::move(connection), ReplConnectionContext(++next_id, this)); + interactions++; + } + + for (auto it = connections.begin(); it != connections.end();) { + auto &[connection, context] = *it; + auto status = connection.process_with(context); + if (status == ReplConnection::DISCONNECTED) { + it = connections.erase(it); + gd::print("Scheme repl client disconnected."); + } else { + it++; + interactions += (status != ReplConnection::IDLE) ? 1 : 0; + } + } + return interactions > 0; +} diff --git a/src/repl/mediator.hpp b/src/repl/mediator.hpp new file mode 100644 index 0000000..8dc7e9a --- /dev/null +++ b/src/repl/mediator.hpp @@ -0,0 +1,47 @@ +#ifndef GODOT_S7_SCHEME_REPL_REQUEST_MEDIATOR_HPP +#define GODOT_S7_SCHEME_REPL_REQUEST_MEDIATOR_HPP + +#include "connection.hpp" +#include "message.hpp" +#include "node_registry.hpp" +#include "request_compiler.hpp" +#include "thread_safe_queue.hpp" +#include +#include + +class ReplMediator { +public: + using MessageQueue = ThreadSafeQueue; + + ReplMediator(godot::Ref server, godot::Callable eval_async_continuation); + +public: + /** + * Process all pending interactions. + * @return true when at least one interaction was processed, false otherwise. + */ + bool mediate(MessageQueue &queue); + +private: + friend struct ReplMessageHandler; + + struct ReplConnectionContext : ReplConnection::Context { + uint64_t connection_id; + ReplMediator *mediator; + ReplConnectionContext(uint64_t id, ReplMediator *mediator) : + connection_id(id), mediator(mediator) {} + ReplRequestCompiler &compiler() override { return mediator->request_compiler; }; + bool eval_async(const godot::String &code, uint64_t scheme_node_id) override; + }; + + friend struct ReplMediator::ReplConnectionContext; + +private: + godot::Ref server; + godot::Callable eval_async_continuation; + std::shared_ptr node_registry; + std::vector> connections; + ReplRequestCompiler request_compiler; + uint64_t next_id = 0; +}; +#endif diff --git a/src/repl/message.hpp b/src/repl/message.hpp new file mode 100644 index 0000000..dd61b86 --- /dev/null +++ b/src/repl/message.hpp @@ -0,0 +1,39 @@ +#ifndef GODOT_S7_SCHEME_REPL_MESSAGE_HPP +#define GODOT_S7_SCHEME_REPL_MESSAGE_HPP + +#include +#include +#include + +struct ReplMessage { + static ReplMessage publish_node(godot::StringName &&node_name, uint64_t node_id) { + return ReplMessage{ PublishNode{ std::move(node_name), node_id } }; + } + + static ReplMessage unpublish_node(uint64_t node_id) { + return ReplMessage{ UnpublishNode{ node_id } }; + } + + static ReplMessage eval_response(uint64_t connection_id, godot::String &&result) { + return ReplMessage{ EvalResponse{ connection_id, std::move(result) } }; + } + + struct PublishNode { + godot::StringName node_name; + uint64_t node_id; + }; + + struct UnpublishNode { + uint64_t node_id; + }; + + struct EvalResponse { + uint64_t connection_id; + godot::String result; + }; + + using Payload = std::variant; + Payload payload; +}; + +#endif diff --git a/src/repl/node_registry.cpp b/src/repl/node_registry.cpp new file mode 100644 index 0000000..d75198a --- /dev/null +++ b/src/repl/node_registry.cpp @@ -0,0 +1,44 @@ +#include "node_registry.hpp" +#include "debug.hpp" +#include + +using namespace godot; + +std::optional ReplNodeRegistry::get_most_recent() { + if (nodes.empty()) { + return std::nullopt; + } + return nodes.back(); +} + +std::vector ReplNodeRegistry::get_available_node_names() { + auto result = std::vector(); + for (const auto &node : nodes) { + result.push_back(node.node_name); + } + return result; +} + +std::optional ReplNodeRegistry::find_node_by_name(const String& node_name) { + for (const auto &node : nodes) { + if (node.node_name == node_name) { + return node; + } + } + return std::nullopt; +} + +void ReplNodeRegistry::register_node(uint64_t node_id, String &&node_name) { + DEBUG_REPL("Scheme node ", node_name, " is available for repl interaction."); + nodes.emplace_back(NodeRecord{ node_id, std::move(node_name) }); +} + +void ReplNodeRegistry::unregister_node(uint64_t node_id) { + for (auto it = nodes.cbegin(); it != nodes.cend(); ++it) { + if (it->node_id == node_id) { + DEBUG_REPL("Scheme node ", it->node_name, " is no longer available for repl interaction."); + nodes.erase(it); + break; + } + } +} diff --git a/src/repl/node_registry.hpp b/src/repl/node_registry.hpp new file mode 100644 index 0000000..2c1de01 --- /dev/null +++ b/src/repl/node_registry.hpp @@ -0,0 +1,24 @@ +#ifndef GODOT_S7_SCHEME_REPL_NODE_REGISTRY_HPP +#define GODOT_S7_SCHEME_REPL_NODE_REGISTRY_HPP + +#include +#include +#include + +class ReplNodeRegistry { +public: + struct NodeRecord { + uint64_t node_id; + godot::String node_name; + }; + + std::optional get_most_recent(); + std::vector get_available_node_names(); + std::optional find_node_by_name(const godot::String& node_name); + void register_node(uint64_t node_id, godot::String &&node_name); + void unregister_node(uint64_t node_id); + +private: + std::vector nodes; +}; +#endif diff --git a/src/repl/request_compiler.cpp b/src/repl/request_compiler.cpp new file mode 100644 index 0000000..a590207 --- /dev/null +++ b/src/repl/request_compiler.cpp @@ -0,0 +1,76 @@ +#include "request_compiler.hpp" +#include "../ffi.hpp" +#include "debug.hpp" +#include "gen/s7_scheme_repl_string.hpp" +#include + +using namespace godot; + +template +std::pair eval_capturing_error_output(s7_scheme *sc, Function f) { + auto string_port = s7_open_output_string(sc); + auto previous_error_port = s7_set_current_error_port(sc, string_port); + auto result = f(sc); + s7_set_current_error_port(sc, previous_error_port); + auto output = s7_output_string(sc, string_port); + return std::make_pair(scheme_string_to_godot_string(output), result); +} + +ReplRequestCompiler::ReplRequestCompiler() { + compile_geiser_request = scheme.make_symbol("compile-geiser-request"); + auto [output, _] = eval_capturing_error_output(scheme.get(), [](auto sc) { + DEBUG_REPL(s7_scheme_repl_string); + s7_load_c_string(sc, + s7_scheme_repl_string, + strlen(s7_scheme_repl_string)); + return nullptr; + }); + if (!output.is_empty()) { + gd::printerr(output); + } +} + +ReplRequestCompiler::~ReplRequestCompiler() { + compile_geiser_request = nullptr; +} + +String ReplRequestCompiler::eval(const String &compiled_request) { + auto sc = scheme.get(); + auto res = scheme.eval(compiled_request); + return scheme_object_to_godot_string(sc, res); +} + +error_output_and_response ReplRequestCompiler::compile_request(const PackedByteArray &request) { + auto compile_geiser_request = scheme.value_of(this->compile_geiser_request); + if (!s7_is_procedure(compile_geiser_request)) { + return std::make_pair("repl script is missing a compile-geiser-request function!", ""); + } + + auto sc = scheme.get(); + auto [output, compiled_request] = + eval_capturing_error_output(sc, + [compile_geiser_request, &request](auto sc) { + auto args = s7_cons(sc, + s7_make_string_wrapper_with_length( + sc, + reinterpret_cast(request.ptr()), + static_cast(request.size())), + s7_nil(sc)); + return s7_call_with_location( + sc, + compile_geiser_request, + args, + __func__, + __FILE__, + __LINE__); + }); + + if (!s7_is_string(compiled_request)) { + DEBUG_REPL(scheme_object_to_godot_string(sc, compiled_request)); + return std::make_pair(output, scheme_object_to_godot_string(sc, compiled_request)); + } + + auto code_string = scheme_string_to_godot_string(compiled_request); + DEBUG_REPL("```\n", code_string, "\n```"); + return std::make_pair(output, code_string); +} diff --git a/src/repl/request_compiler.hpp b/src/repl/request_compiler.hpp new file mode 100644 index 0000000..3e2f6f0 --- /dev/null +++ b/src/repl/request_compiler.hpp @@ -0,0 +1,23 @@ +#ifndef GODOT_S7_SCHEME_REPL_REQUEST_COMPILER_HPP +#define GODOT_S7_SCHEME_REPL_REQUEST_COMPILER_HPP + +#include "../s7.hpp" +#include +#include + +using error_output_and_response = std::pair; + +class ReplRequestCompiler { +public: + ReplRequestCompiler(); + ~ReplRequestCompiler(); + +public: + error_output_and_response compile_request(const godot::PackedByteArray &request); + godot::String eval(const godot::String& compiled_request); + +private: + s7_protected_ptr compile_geiser_request; + s7 scheme; +}; +#endif diff --git a/src/repl/thread_safe_queue.hpp b/src/repl/thread_safe_queue.hpp new file mode 100644 index 0000000..3cc09ea --- /dev/null +++ b/src/repl/thread_safe_queue.hpp @@ -0,0 +1,30 @@ +#ifndef GODOT_S7_SCHEME_THREAD_SAFE_QUEUE_HPP +#define GODOT_S7_SCHEME_THREAD_SAFE_QUEUE_HPP + +#include +#include +#include + +template +class ThreadSafeQueue { +private: + std::queue queue; + std::mutex mutex; + +public: + void push(T&& item) { + std::lock_guard lock(mutex); + queue.emplace(std::move(item)); + } + + std::optional pop() { + std::unique_lock lock(mutex); + if (queue.empty()) { + return std::nullopt; + } + T item = std::move(queue.front()); + queue.pop(); + return item; + } +}; +#endif diff --git a/src/s7.cpp b/src/s7.cpp index 7df679f..f9bed2e 100644 --- a/src/s7.cpp +++ b/src/s7.cpp @@ -2,10 +2,11 @@ #include "s7.hpp" #include "debug_macros.hpp" #include -#include #include -class godot::s7_scheme_context { +using namespace godot; + +class s7_scheme_context { public: void print_error(uint8_t char_code) { if (char_code == '\n') { @@ -24,8 +25,6 @@ class godot::s7_scheme_context { std::vector error_buffer; }; -using namespace godot; - void add_scheme_mapping(s7_scheme *sc, s7_scheme_context *scheme) { s7_define_constant(sc, "*ctx*", s7_make_c_pointer(sc, scheme)); } @@ -61,9 +60,7 @@ void s7::load_string(const String &str) const { } s7_pointer s7::eval(const String &code) const { - auto sc = get(); - auto str = code.utf8(); - return s7_eval_c_string(sc, str); + return s7_eval_c_string(get(), code.utf8()); } s7_pointer s7::define(const char *name, s7_pointer value, const char *help) const { @@ -71,8 +68,7 @@ s7_pointer s7::define(const char *name, s7_pointer value, const char *help) cons return s7_define_variable_with_documentation(sc, name, value, help); } -s7_pointer s7::define_constant_with_documentation( - const char *name, s7_pointer value, const char *help) const { +s7_pointer s7::define_constant_with_documentation(const char *name, s7_pointer value, const char *help) const { auto sc = get(); return s7_define_constant_with_documentation(sc, name, value, help); } diff --git a/src/s7.hpp b/src/s7.hpp index 585c1c5..661ce9e 100644 --- a/src/s7.hpp +++ b/src/s7.hpp @@ -7,78 +7,80 @@ typedef void (*s7_output_port_function_t)(s7_scheme *sc, uint8_t c, s7_pointer port); -namespace godot { typedef std::shared_ptr s7_protected_ptr; inline s7_protected_ptr s7_gc_protected(s7_scheme *sc, s7_pointer p) { - auto l = s7_gc_protect(sc, p); - s7_protected_ptr ptr(p, [sc, l]([[maybe_unused]] auto p) { s7_gc_unprotect_at(sc, l); }); - return ptr; + auto l = s7_gc_protect(sc, p); + s7_protected_ptr ptr(p, [sc, l]([[maybe_unused]] auto p) { s7_gc_unprotect_at(sc, l); }); + return ptr; } class s7_scheme_context; class s7 { public: - s7(const s7 &other) = default; - s7(); - - [[nodiscard]] s7_scheme *get() const { return scheme.get(); }; - - s7_pointer define(const char *name, s7_pointer value, const char *documentation) const; - s7_pointer define_constant_with_documentation( - const char *name, - s7_pointer value, - const char *documentation) const; - [[nodiscard]] s7_pointer eval(const String &code) const; - void load_string(const String &code) const; - void set_current_error_port_function(s7_output_port_function_t f) const; - - s7_protected_ptr make_symbol(const char *name) const { - auto sc = get(); - return s7_gc_protected(sc, s7_make_symbol(sc, name)); - } - - template - s7_pointer call_optional(S what) const { - auto sc = get(); - auto proc = _scheme_resolve(sc, what); - return s7_is_procedure(proc) - ? s7_call_with_location(sc, proc, s7_nil(sc), __func__, __FILE__, __LINE__) - : s7_unspecified(sc); - } - - template - s7_pointer call(S what, T arg) const { - auto sc = get(); - auto proc = _scheme_resolve(sc, what); - return s7_call_with_location(sc, - proc, - s7_cons(sc, _scheme_value_of(sc, arg), s7_nil(sc)), - __func__, - __FILE__, - __LINE__); - } + s7(const s7 &other) = default; + s7(); + + [[nodiscard]] s7_scheme *get() const { return scheme.get(); }; + + s7_pointer define(const char *name, s7_pointer value, const char *documentation) const; + s7_pointer define_constant_with_documentation( + const char *name, + s7_pointer value, + const char *documentation) const; + [[nodiscard]] s7_pointer eval(const godot::String &code) const; + void load_string(const godot::String &code) const; + void set_current_error_port_function(s7_output_port_function_t f) const; + + s7_protected_ptr make_symbol(const char *name) const { + auto sc = get(); + return s7_gc_protected(sc, s7_make_symbol(sc, name)); + } + + s7_pointer value_of(const s7_protected_ptr &symbol) const { + return s7_symbol_value(get(), symbol.get()); + } + + template + s7_pointer call_optional(S what) const { + auto sc = get(); + auto proc = _scheme_resolve(sc, what); + return s7_is_procedure(proc) + ? s7_call_with_location(sc, proc, s7_nil(sc), __func__, __FILE__, __LINE__) + : s7_unspecified(sc); + } + + template + s7_pointer call(S what, T arg) const { + auto sc = get(); + auto proc = _scheme_resolve(sc, what); + return s7_call_with_location(sc, + proc, + s7_cons(sc, _scheme_value_of(sc, arg), s7_nil(sc)), + __func__, + __FILE__, + __LINE__); + } private: - static s7_pointer _scheme_value_of(s7_scheme *sc, double arg) { - return s7_make_real(sc, arg); - } + static s7_pointer _scheme_value_of(s7_scheme *sc, double arg) { + return s7_make_real(sc, arg); + } - static s7_pointer _scheme_value_of(s7_scheme *sc, int32_t arg) { - return s7_make_integer(sc, arg); - } + static s7_pointer _scheme_value_of(s7_scheme *sc, int32_t arg) { + return s7_make_integer(sc, arg); + } - static s7_pointer _scheme_resolve(s7_scheme *sc, s7_pointer symbol) { - return s7_symbol_value(sc, symbol); - } + static s7_pointer _scheme_resolve(s7_scheme *sc, s7_pointer symbol) { + return s7_symbol_value(sc, symbol); + } - static s7_pointer _scheme_resolve(s7_scheme *sc, const char *name) { - return s7_name_to_value(sc, name); - } + static s7_pointer _scheme_resolve(s7_scheme *sc, const char *name) { + return s7_name_to_value(sc, name); + } - std::shared_ptr scheme; - std::shared_ptr scheme_context; + std::shared_ptr scheme; + std::shared_ptr scheme_context; }; -} //namespace godot #endif //GODOT_S7_SCHEME_S7_HPP diff --git a/src/scheme.cpp b/src/scheme.cpp index 0482e5e..ed1638a 100644 --- a/src/scheme.cpp +++ b/src/scheme.cpp @@ -1,6 +1,7 @@ #include "scheme.hpp" #include "ffi.hpp" #include "godot_cpp/variant/utility_functions.hpp" +#include "scheme_repl_server.hpp" using namespace godot; @@ -12,6 +13,31 @@ Scheme::Scheme() { Scheme::~Scheme() { _process_symbol = nullptr; } +void Scheme::_ready() { + load_prelude(); + load_script(); +} + +void Scheme::_process(double delta) { + if (_process_symbol) { + scheme.call(_process_symbol.get(), delta); + } +} +void Scheme::_enter_tree() { + // TODO: move initialization here + SchemeReplServer::get_singleton()->publish_node(this); + Node::_enter_tree(); +} + +void Scheme::_exit_tree() { + SchemeReplServer::get_singleton()->unpublish_node(this); + + if (_process_symbol) { + auto _ = scheme.call_optional("_exit_tree"); + } + Node::_exit_tree(); +} + void Scheme::define( const godot::String &name, const godot::Variant &value, @@ -22,27 +48,26 @@ void Scheme::define( void Scheme::set_scheme_script(const Ref &p_scheme_script) { scheme_script = p_scheme_script; if (is_node_ready()) { - _ready(); + load_script(); } } -void Scheme::_ready() { +void Scheme::load_prelude() { for (int i = 0; i < prelude.size(); ++i) { auto script = Object::cast_to(prelude[i]); DEV_ASSERT(script != nullptr); load(script); } +} +void Scheme::load_script() { if (scheme_script.is_null()) { _process_symbol = nullptr; - set_process(false); return; } load(scheme_script.ptr()); - _process_symbol = scheme.make_symbol("_process"); - set_process(true); } void Scheme::load(const godot::SchemeScript *script) const { @@ -53,23 +78,15 @@ void Scheme::load_string(const String &code) const { scheme.load_string(code); } -void Scheme::_process(double delta) { - if (_process_symbol) { - scheme.call(_process_symbol.get(), delta); - } -} - -void Scheme::_exit_tree() { - if (_process_symbol) { - auto res = scheme.call_optional("_exit_tree"); - } - Node::_exit_tree(); -} - Variant Scheme::eval(const String &code) { return scheme_to_variant(scheme.get(), scheme.eval(code)); } +void Scheme::eval_async(const String &code, const Callable &continuation) { + auto result = scheme_object_to_godot_string(scheme.get(), scheme.eval(code)); + continuation.call_deferred(result); +} + s7_pointer array_to_list(s7_scheme *sc, const Array &array) { auto list = s7_nil(sc); auto arg_count = static_cast(array.size()); @@ -117,6 +134,7 @@ void Scheme::_bind_methods() { &Scheme::define, DEFVAL("")); ClassDB::bind_method(D_METHOD("eval", "p_code"), &Scheme::eval); + ClassDB::bind_method(D_METHOD("eval_async", "p_code", "p_continuation"), &Scheme::eval_async); ClassDB::bind_method(D_METHOD("apply", "p_symbol", "p_args"), &Scheme::apply, DEFVAL(Array())); diff --git a/src/scheme.hpp b/src/scheme.hpp index ede0ca9..6a4c36b 100644 --- a/src/scheme.hpp +++ b/src/scheme.hpp @@ -8,36 +8,45 @@ namespace godot { class Scheme : public Node { - GDCLASS(Scheme, Node) + GDCLASS(Scheme, Node) public: - Scheme(); - ~Scheme() override; - - void _ready() override; - void _process(double delta) override; - void _exit_tree() override; - - void define(const String &name, const Variant &value, const String &help = "") const; - void load(const SchemeScript *script) const; - void load_string(const String &code) const; - Variant eval(const String &code); - Variant apply(const String &symbol, const Array &args) const; - void set_prelude(const TypedArray &p_prelude) { prelude = p_prelude; } - [[nodiscard]] TypedArray get_prelude() const { return prelude; } - void set_scheme_script(const Ref &p_scheme_script); - [[nodiscard]] Ref get_scheme_script() const { return scheme_script; }; - - [[nodiscard]] const s7 &get_s7() const { return scheme; } + Scheme(); + ~Scheme() override; + + void _ready() override; + void _process(double delta) override; + void _enter_tree() override; + void _exit_tree() override; + + void define(const String &name, const Variant &value, const String &help = "") const; + void load(const SchemeScript *script) const; + void load_string(const String &code) const; + Variant eval(const String &code); + /** + * Process an async evaluation request from repl and calls [continuation] with the result. + */ + void eval_async(const String &code, const Callable &continuation); + Variant apply(const String &symbol, const Array &args) const; + void set_prelude(const TypedArray &p_prelude) { prelude = p_prelude; } + [[nodiscard]] TypedArray get_prelude() const { return prelude; } + void set_scheme_script(const Ref &p_scheme_script); + [[nodiscard]] Ref get_scheme_script() const { return scheme_script; }; + + [[nodiscard]] const s7 &get_s7() const { return scheme; } protected: - static void _bind_methods(); + static void _bind_methods(); + +private: + void load_prelude(); + void load_script(); private: - TypedArray prelude; - Ref scheme_script; - s7_protected_ptr _process_symbol; - s7 scheme; + TypedArray prelude; + Ref scheme_script; + s7_protected_ptr _process_symbol; + s7 scheme; }; } // namespace godot diff --git a/src/scheme_object.hpp b/src/scheme_object.hpp index a40a278..700832c 100644 --- a/src/scheme_object.hpp +++ b/src/scheme_object.hpp @@ -11,23 +11,26 @@ class SchemeObject : public RefCounted { GDCLASS(SchemeObject, RefCounted) public: - SchemeObject() : sc(nullptr), scheme_ptr(nullptr) {} - SchemeObject(s7_scheme* sc, s7_pointer shared) : sc(sc), scheme_ptr(std::move(s7_gc_protected(sc, shared))){} + SchemeObject() : + sc(nullptr), scheme_ptr(nullptr) {} + SchemeObject(s7_scheme *sc, s7_pointer shared) : + sc(sc), scheme_ptr(s7_gc_protected(sc, shared)) {} - bool belongs_to(const s7_scheme* scheme) const { + bool belongs_to(const s7_scheme *scheme) const { return sc == scheme; } [[nodiscard]] s7_pointer get_scheme_ptr() const { return scheme_ptr.get(); } + protected: static void _bind_methods(); private: - const s7_scheme* sc; + const s7_scheme *sc; s7_protected_ptr scheme_ptr; }; -} +} //namespace godot #endif //SCHEME_OBJECT_H diff --git a/src/scheme_repl_server.cpp b/src/scheme_repl_server.cpp new file mode 100644 index 0000000..820ae7e --- /dev/null +++ b/src/scheme_repl_server.cpp @@ -0,0 +1,106 @@ +#include "scheme_repl_server.hpp" +#include "repl/mediator.hpp" +#include "scheme.hpp" +#include +#include + +using namespace godot; +using gd = UtilityFunctions; + +void SchemeReplServer::publish_node(const Scheme *node) { + if (thread.is_null()) { + return; + } + auto node_name = node->get_path().slice(-2).get_concatenated_names(); + message_queue.push(ReplMessage::publish_node(std::move(node_name), node->get_instance_id())); +} + +void SchemeReplServer::unpublish_node(const Scheme *node) { + if (thread.is_null()) { + return; + } + message_queue.push(ReplMessage::unpublish_node(node->get_instance_id())); +} + +void SchemeReplServer::reply(String result, uint64_t connection_id) { + if (thread.is_null()) { + return; + } + message_queue.push(ReplMessage::eval_response(connection_id, std::move(result))); +} + +void SchemeReplServer::server_loop(int tcp_port, const String &tcp_bind_address) { + Ref tcp_server; + tcp_server.instantiate(); + + auto error = tcp_server->listen(tcp_port, tcp_bind_address); + ERR_FAIL_COND_MSG( + error != OK, + ("Failed to start scheme repl server: " + gd::error_string(error))); + + gd::print("Scheme repl server listening on local port ", tcp_server->get_local_port()); + + auto mediator = ReplMediator(tcp_server, Callable::create(this, "reply")); + while (!exit_thread) { + if (!mediator.mediate(message_queue)) { + OS::get_singleton()->delay_msec(50); + } + } + tcp_server->stop(); +} + +std::optional> parse_repl_args() { + // TODO: accept --s7-tcp-address=
+ String tcp_bind_address = "127.0.0.1"; + for (const auto &arg : OS::get_singleton()->get_cmdline_args()) { + if (arg.begins_with("--s7-tcp-port")) { + auto parts = arg.split("="); + auto tcp_port = parts.size() > 1 ? parts[1].to_int() : 0; + return std::make_pair(tcp_port, tcp_bind_address); + } + } + return std::nullopt; +} + +Error SchemeReplServer::start() { + ERR_FAIL_COND_V_MSG(thread.is_valid(), ERR_BUG, "Scheme repl server can only be started once!"); + + auto repl_args = parse_repl_args(); + if (!repl_args) { + return Error::OK; + } + + const auto &[tcp_port, tcp_bind_address] = *repl_args; + + exit_thread = false; + thread.instantiate(); + return thread->start( + Callable::create(this, "server_loop").bind(tcp_port, tcp_bind_address), + Thread::PRIORITY_LOW); +} + +void SchemeReplServer::stop() { + if (thread.is_null()) { + return; + } + + exit_thread = true; + thread->wait_to_finish(); + + thread.unref(); +} + +SchemeReplServer *SchemeReplServer::singleton = NULL; + +SchemeReplServer *SchemeReplServer::get_singleton() { + return singleton; +} + +SchemeReplServer::SchemeReplServer() { + singleton = this; +} + +void SchemeReplServer::_bind_methods() { + ClassDB::bind_method(D_METHOD("server_loop"), &SchemeReplServer::server_loop); + ClassDB::bind_method(D_METHOD("reply"), &SchemeReplServer::reply); +} diff --git a/src/scheme_repl_server.hpp b/src/scheme_repl_server.hpp new file mode 100644 index 0000000..141c2ee --- /dev/null +++ b/src/scheme_repl_server.hpp @@ -0,0 +1,37 @@ +#ifndef GODOT_S7_SCHEME_SCHEME_REPL_SERVER_H +#define GODOT_S7_SCHEME_SCHEME_REPL_SERVER_H + +#include "repl/mediator.hpp" +#include +#include + +namespace godot { + +class Scheme; + +class SchemeReplServer : public Object { + GDCLASS(SchemeReplServer, Object); + +public: // public API + static SchemeReplServer *get_singleton(); + void publish_node(const Scheme *node); + void unpublish_node(const Scheme *node); + void reply(String result, uint64_t connection_id); + +private: + static SchemeReplServer *singleton; + bool exit_thread; + Ref thread; + ReplMediator::MessageQueue message_queue; + +public: // extension initialization API + SchemeReplServer(); + Error start(); + void stop(); + +protected: + static void _bind_methods(); + void server_loop(int tcp_port, const String &tcp_bind_address); +}; +} //namespace godot +#endif //GODOT_S7_SCHEME_SCHEME_REPL_SERVER_H diff --git a/src/scheme_script.hpp b/src/scheme_script.hpp index cc689e8..abe7d8d 100644 --- a/src/scheme_script.hpp +++ b/src/scheme_script.hpp @@ -9,7 +9,8 @@ class SchemeScript : public Resource { public: SchemeScript(); - SchemeScript(const String &code) : code(code) {} + SchemeScript(const String &code) : + code(code) {} ~SchemeScript(); const String &get_code() const { return code; }