musa-dsl 0.30.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- metadata +87 -8
data/lib/musa-dsl/repl/repl.rb
CHANGED
|
@@ -3,10 +3,217 @@ require 'pathname'
|
|
|
3
3
|
require 'stringio'
|
|
4
4
|
|
|
5
5
|
module Musa
|
|
6
|
+
# REPL (Read-Eval-Print Loop) infrastructure for live coding.
|
|
7
|
+
#
|
|
8
|
+
# The REPL module provides a TCP-based server that enables live coding by accepting
|
|
9
|
+
# Ruby code over the network, executing it in the DSL context, and returning results
|
|
10
|
+
# or errors to the client.
|
|
11
|
+
#
|
|
12
|
+
# ## Architecture
|
|
13
|
+
#
|
|
14
|
+
# The REPL acts as a bridge between code editors (via MusaLCE clients) and the
|
|
15
|
+
# running Musa DSL environment:
|
|
16
|
+
#
|
|
17
|
+
# Editor → MusaLCE Client → TCP (port 1327) → REPL Server → DSL Context
|
|
18
|
+
# ↓
|
|
19
|
+
# Results/Errors
|
|
20
|
+
#
|
|
21
|
+
# ## Protocol Details
|
|
22
|
+
#
|
|
23
|
+
# The REPL uses a line-based protocol over TCP:
|
|
24
|
+
#
|
|
25
|
+
# ### Client to Server
|
|
26
|
+
#
|
|
27
|
+
# - **#path**: Start path block (optional)
|
|
28
|
+
# - *file path*: Path to the user's file being edited
|
|
29
|
+
# - **#begin**: Start code block
|
|
30
|
+
# - *code lines*: Ruby code to execute (literal `#begin`/`#end` must be escaped as `##begin`/`##end`)
|
|
31
|
+
# - **#end**: Execute accumulated code block
|
|
32
|
+
#
|
|
33
|
+
# ### Server to Client
|
|
34
|
+
#
|
|
35
|
+
# - **//echo**: Start echo block (code about to be executed)
|
|
36
|
+
# - **//error**: Start error block
|
|
37
|
+
# - **//backtrace**: Start backtrace section within error block
|
|
38
|
+
# - **//end**: End current block
|
|
39
|
+
# - *regular lines*: Output from code execution (puts, etc.)
|
|
40
|
+
# - **//*text***: Escaped lines starting with // (clients remove one `//`)
|
|
41
|
+
#
|
|
42
|
+
# ### Example Session
|
|
43
|
+
#
|
|
44
|
+
# Client → Server:
|
|
45
|
+
# #path
|
|
46
|
+
# /Users/me/composition.rb
|
|
47
|
+
# #begin
|
|
48
|
+
# puts "Starting..."
|
|
49
|
+
# at 0 { play :C4 }
|
|
50
|
+
# #end
|
|
51
|
+
#
|
|
52
|
+
# Server → Client:
|
|
53
|
+
# //echo
|
|
54
|
+
# puts "Starting..."
|
|
55
|
+
# at 0 { play :C4 }
|
|
56
|
+
# //end
|
|
57
|
+
# Starting...
|
|
58
|
+
#
|
|
59
|
+
# ## Integration with MusaLCE
|
|
60
|
+
#
|
|
61
|
+
# The REPL is designed to work seamlessly with MusaLCE (Musa Live Coding Environment)
|
|
62
|
+
# clients for various editors:
|
|
63
|
+
#
|
|
64
|
+
# - **MusaLCEClientForVSCode**: Visual Studio Code extension
|
|
65
|
+
# - **MusaLCEClientForAtom**: Atom editor plugin
|
|
66
|
+
# - **musalce-server**: Server integrating with DAWs (Bitwig, Ableton Live)
|
|
67
|
+
#
|
|
68
|
+
# ## File Path Injection
|
|
69
|
+
#
|
|
70
|
+
# When a client sends a file path via the `#path` command, the REPL injects it
|
|
71
|
+
# as `@user_pathname` (Pathname object) into the execution context. This allows
|
|
72
|
+
# the DSL to implement relative `require_relative` calls based on the editor's
|
|
73
|
+
# current file location.
|
|
74
|
+
#
|
|
75
|
+
# ## Use Cases
|
|
76
|
+
#
|
|
77
|
+
# - Live coding performances with real-time code evaluation
|
|
78
|
+
# - Interactive composition development with DAW synchronization
|
|
79
|
+
# - Remote control of running compositions
|
|
80
|
+
# - Educational demonstrations and workshops
|
|
81
|
+
#
|
|
82
|
+
# @see REPL Main REPL server class
|
|
83
|
+
# @see CustomizableDSLContext Mixin for DSL contexts
|
|
84
|
+
# @see https://github.com/javier-sy/musa-dsl-examples/tree/master/musalce-server Production server integrating REPL with DAWs
|
|
6
85
|
module REPL
|
|
86
|
+
# TCP-based REPL server for live coding.
|
|
87
|
+
#
|
|
88
|
+
# The REPL class implements a multi-threaded TCP server that accepts connections
|
|
89
|
+
# from live coding clients (like MusaLCE for VSCode/Atom), receives Ruby code,
|
|
90
|
+
# executes it in a bound DSL context, and sends back results or exceptions.
|
|
91
|
+
#
|
|
92
|
+
# ## Threading Model
|
|
93
|
+
#
|
|
94
|
+
# - **Main thread**: Accepts client connections
|
|
95
|
+
# - **Client threads**: One per connected client, handles requests
|
|
96
|
+
# - **Mutex protection**: Serializes code execution for safety (@@repl_mutex)
|
|
97
|
+
#
|
|
98
|
+
# ## Binding Options
|
|
99
|
+
#
|
|
100
|
+
# The REPL can be bound to a DSL context in two ways:
|
|
101
|
+
#
|
|
102
|
+
# 1. **DynamicProxy**: For transparent method delegation (recommended for complex DSLs)
|
|
103
|
+
# 2. **Direct Binding**: Pass a Ruby Binding object directly for inline context setup
|
|
104
|
+
#
|
|
105
|
+
# ## Protocol Flow
|
|
106
|
+
#
|
|
107
|
+
# 1. **Optional path**: `#path\n/path/to/file\n#begin\n` (injects @user_pathname)
|
|
108
|
+
# 2. **Code block**: `#begin\ncode\nmore code\n#end\n`
|
|
109
|
+
# 3. **Server responses**:
|
|
110
|
+
#
|
|
111
|
+
# - Echo: `//echo\ncode\n//end\n`
|
|
112
|
+
# - Error: `//error\nerror details\n//backtrace\nstack\n//end\n`
|
|
113
|
+
# - Output: Regular lines (from puts calls)
|
|
114
|
+
#
|
|
115
|
+
# ## Error Handling
|
|
116
|
+
#
|
|
117
|
+
# Errors are captured and formatted with:
|
|
118
|
+
#
|
|
119
|
+
# - Source code context (3 lines: before, error line, after)
|
|
120
|
+
# - Error class and message
|
|
121
|
+
# - Filtered backtrace (shows only REPL-executed code)
|
|
122
|
+
# - Optional ANSI syntax highlighting (via highlight_exception parameter)
|
|
123
|
+
#
|
|
124
|
+
# ## Integration with Sequencer
|
|
125
|
+
#
|
|
126
|
+
# If the bound context has a sequencer with on_error support, the REPL
|
|
127
|
+
# automatically hooks into it to report async errors during playback.
|
|
128
|
+
#
|
|
129
|
+
# @example With DynamicProxy (complex DSL)
|
|
130
|
+
# class MyDSL
|
|
131
|
+
# include Musa::REPL::CustomizableDSLContext
|
|
132
|
+
# # ... DSL methods ...
|
|
133
|
+
# end
|
|
134
|
+
#
|
|
135
|
+
# repl = Musa::REPL::REPL.new(
|
|
136
|
+
# bind: Musa::Extension::DynamicProxy::DynamicProxy.new(MyDSL.new),
|
|
137
|
+
# port: 1327
|
|
138
|
+
# )
|
|
139
|
+
#
|
|
140
|
+
# @example With direct Binding (musalce-server pattern)
|
|
141
|
+
# sequencer.with(keep_block_context: false) do
|
|
142
|
+
# # Define DSL methods in this context
|
|
143
|
+
# def play(note); end
|
|
144
|
+
# def at(pos, &block); end
|
|
145
|
+
#
|
|
146
|
+
# # Create REPL with this binding
|
|
147
|
+
# @repl = Musa::REPL::REPL.new(binding, highlight_exception: false)
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# @example With after_eval callback
|
|
151
|
+
# dsl_context = MyDSL.new
|
|
152
|
+
# context_proxy = Musa::Extension::DynamicProxy::DynamicProxy.new(dsl_context)
|
|
153
|
+
# repl = REPL.new(
|
|
154
|
+
# bind: context_proxy,
|
|
155
|
+
# after_eval: -> (source) { log_execution(source) }
|
|
156
|
+
# )
|
|
157
|
+
#
|
|
158
|
+
# @see CustomizableDSLContext For binding DSL contexts with DynamicProxy
|
|
159
|
+
# @see Musa::Extension::DynamicProxy::DynamicProxy Proxy wrapper for DSL contexts
|
|
160
|
+
# @see https://github.com/javier-sy/musa-dsl-examples/tree/master/musalce-server Production example using direct binding
|
|
7
161
|
class REPL
|
|
162
|
+
# Class-level mutex for serializing code execution.
|
|
163
|
+
#
|
|
164
|
+
# Ensures only one code block executes at a time across all clients.
|
|
8
165
|
@@repl_mutex = Mutex.new
|
|
9
166
|
|
|
167
|
+
# Creates a new REPL server.
|
|
168
|
+
#
|
|
169
|
+
# The server starts immediately in a background thread, listening for connections
|
|
170
|
+
# on the specified port (default 1327).
|
|
171
|
+
#
|
|
172
|
+
# ## Binding Patterns
|
|
173
|
+
#
|
|
174
|
+
# The first parameter can be:
|
|
175
|
+
#
|
|
176
|
+
# - **Binding**: Ruby binding object (inline DSL setup, see musalce-server)
|
|
177
|
+
# - **DynamicProxy**: Proxy object wrapping a DSL context
|
|
178
|
+
# - **nil**: Binding can be set later via {#bind=}
|
|
179
|
+
#
|
|
180
|
+
# ## Parameter Options
|
|
181
|
+
#
|
|
182
|
+
# Named parameters provide additional configuration:
|
|
183
|
+
#
|
|
184
|
+
# - **port**: TCP port (default: 1327)
|
|
185
|
+
# - **after_eval**: Callback for successful executions
|
|
186
|
+
# - **logger**: Custom logger (creates default if nil)
|
|
187
|
+
# - **highlight_exception**: ANSI colors in errors (default: true, musalce-server uses false)
|
|
188
|
+
#
|
|
189
|
+
# @param bind [Binding, DynamicProxy, nil] execution context (can be set later)
|
|
190
|
+
# @param port [Integer, nil] TCP port to listen on (default: 1327)
|
|
191
|
+
# @param after_eval [Proc, nil] callback invoked after successful code execution
|
|
192
|
+
# @param logger [Logger, nil] logger instance (creates default if nil)
|
|
193
|
+
# @param highlight_exception [Boolean] enable ANSI color in exception output (default: true)
|
|
194
|
+
#
|
|
195
|
+
# @yield [source] Called via after_eval after successful execution
|
|
196
|
+
# @yieldparam source [String] the executed source code
|
|
197
|
+
#
|
|
198
|
+
# @example With DynamicProxy and named parameters
|
|
199
|
+
# dsl_context = MyDSL.new
|
|
200
|
+
# custom_logger = Musa::Logger::Logger.new
|
|
201
|
+
# REPL.new(
|
|
202
|
+
# bind: Musa::Extension::DynamicProxy::DynamicProxy.new(dsl_context),
|
|
203
|
+
# port: 1327,
|
|
204
|
+
# after_eval: -> (src) { log_execution(src) },
|
|
205
|
+
# logger: custom_logger
|
|
206
|
+
# )
|
|
207
|
+
#
|
|
208
|
+
# @example With direct Binding (musalce-server pattern)
|
|
209
|
+
# # Inside a context setup block:
|
|
210
|
+
# @repl = REPL.new(binding, highlight_exception: false)
|
|
211
|
+
#
|
|
212
|
+
# @example Deferred binding
|
|
213
|
+
# repl = REPL.new # Start server without binding
|
|
214
|
+
# # ... later ...
|
|
215
|
+
# context = MyDSL.new
|
|
216
|
+
# repl.bind = Musa::Extension::DynamicProxy::DynamicProxy.new(context)
|
|
10
217
|
def initialize(bind = nil, port: nil, after_eval: nil, logger: nil, highlight_exception: true)
|
|
11
218
|
|
|
12
219
|
self.bind = bind
|
|
@@ -21,14 +228,18 @@ module Musa
|
|
|
21
228
|
@client_threads = []
|
|
22
229
|
@run = true
|
|
23
230
|
|
|
231
|
+
# Start main server thread
|
|
24
232
|
@main_thread = Thread.new do
|
|
25
233
|
@server = TCPServer.new(port)
|
|
26
234
|
begin
|
|
235
|
+
# Accept client connections
|
|
27
236
|
while (@connection = @server.accept) && @run
|
|
237
|
+
# Spawn thread for each client
|
|
28
238
|
@client_threads << Thread.new do
|
|
29
239
|
buffer = nil
|
|
30
240
|
|
|
31
241
|
begin
|
|
242
|
+
# Process lines from client
|
|
32
243
|
while (line = @connection.gets) && @run
|
|
33
244
|
|
|
34
245
|
@logger.warn('REPL') { 'input line is nil; will close connection...' } if line.nil?
|
|
@@ -36,41 +247,52 @@ module Musa
|
|
|
36
247
|
line.chomp!
|
|
37
248
|
case line
|
|
38
249
|
when '#path'
|
|
250
|
+
# Start path block
|
|
39
251
|
buffer = StringIO.new
|
|
40
252
|
|
|
41
253
|
when '#begin'
|
|
254
|
+
# Save path (if provided), start code block
|
|
42
255
|
user_path = buffer&.string
|
|
43
256
|
@bind.receiver.instance_variable_set(:@user_pathname, Pathname.new(user_path)) if user_path
|
|
44
257
|
|
|
45
258
|
buffer = StringIO.new
|
|
46
259
|
|
|
47
260
|
when '#end'
|
|
261
|
+
# Execute accumulated code block
|
|
48
262
|
@@repl_mutex.synchronize do
|
|
49
263
|
@block_source = buffer.string
|
|
50
264
|
|
|
51
265
|
begin
|
|
266
|
+
# Echo code to client
|
|
52
267
|
send_echo @block_source, output: @connection
|
|
268
|
+
|
|
269
|
+
# Execute in DSL context
|
|
53
270
|
@bind.receiver.execute @block_source, '(repl)', 1
|
|
54
271
|
|
|
55
272
|
rescue StandardError, ScriptError => e
|
|
273
|
+
# Handle execution errors
|
|
56
274
|
@logger.warn('REPL') { 'code execution error' }
|
|
57
275
|
@logger.warn('REPL') { e.full_message(highlight: @highlight_exception, order: :top) }
|
|
58
276
|
|
|
59
277
|
send_exception e, output: @connection
|
|
60
278
|
else
|
|
279
|
+
# Success: invoke callback
|
|
61
280
|
after_eval.call @block_source if after_eval
|
|
62
281
|
end
|
|
63
282
|
end
|
|
64
283
|
else
|
|
284
|
+
# Accumulate code lines
|
|
65
285
|
buffer.puts line
|
|
66
286
|
end
|
|
67
287
|
end
|
|
68
288
|
|
|
69
289
|
rescue IOError, Errno::ECONNRESET, Errno::EPIPE => e
|
|
290
|
+
# Connection errors
|
|
70
291
|
@logger.warn('REPL') { 'lost connection' }
|
|
71
292
|
@logger.warn('REPL') { e.full_message(highlight: @highlight_exception, order: :top) }
|
|
72
293
|
|
|
73
294
|
ensure
|
|
295
|
+
# Clean up connection
|
|
74
296
|
@logger.debug('REPL') { "closing connection (running #{@run})" }
|
|
75
297
|
@connection.close
|
|
76
298
|
end
|
|
@@ -78,6 +300,7 @@ module Musa
|
|
|
78
300
|
end
|
|
79
301
|
end
|
|
80
302
|
rescue Errno::ECONNRESET, Errno::EPIPE => e
|
|
303
|
+
# Server socket errors - retry
|
|
81
304
|
@logger.warn('REPL') { 'connection failure while getting server port; will retry...' }
|
|
82
305
|
@logger.warn('REPL') { e.full_message(highlight: @highlight_exception, order: :top) }
|
|
83
306
|
retry
|
|
@@ -86,6 +309,43 @@ module Musa
|
|
|
86
309
|
end
|
|
87
310
|
end
|
|
88
311
|
|
|
312
|
+
# Sets or updates the binding context.
|
|
313
|
+
#
|
|
314
|
+
# The binding context is where code will be executed. The REPL accesses
|
|
315
|
+
# the execution context via `bind.receiver.execute(source, file, line)`:
|
|
316
|
+
#
|
|
317
|
+
# - **Binding**: Uses Ruby's `Binding#receiver` (returns the binding's self)
|
|
318
|
+
# - **DynamicProxy**: Uses `DynamicProxy#receiver` (returns wrapped object)
|
|
319
|
+
#
|
|
320
|
+
# ## Requirements
|
|
321
|
+
#
|
|
322
|
+
# The `bind.receiver` object must implement:
|
|
323
|
+
#
|
|
324
|
+
# - `execute(source, file, line)`: Evaluates source code
|
|
325
|
+
# - Optionally `sequencer.on_error`: For async error reporting
|
|
326
|
+
#
|
|
327
|
+
# ## Sequencer Integration
|
|
328
|
+
#
|
|
329
|
+
# If `bind.receiver` has a sequencer with `on_error` support, the REPL
|
|
330
|
+
# automatically hooks into it to report sequencer errors to the client
|
|
331
|
+
# during playback.
|
|
332
|
+
#
|
|
333
|
+
# @param bind [Binding, DynamicProxy, Object] binding context with `receiver`
|
|
334
|
+
# @return [Object] the bind object
|
|
335
|
+
#
|
|
336
|
+
# @raise [RuntimeError] if bind is already set
|
|
337
|
+
#
|
|
338
|
+
# @note Can only be set once
|
|
339
|
+
# @note Automatically hooks into `bind.receiver.sequencer.on_error` if available
|
|
340
|
+
#
|
|
341
|
+
# @example With Ruby Binding
|
|
342
|
+
# # binding.receiver returns the DSLContext instance
|
|
343
|
+
# repl.bind = binding # Inside DSL context
|
|
344
|
+
#
|
|
345
|
+
# @example With DynamicProxy
|
|
346
|
+
# # proxy.receiver returns the wrapped object
|
|
347
|
+
# dsl_context = MyDSL.new
|
|
348
|
+
# repl.bind = Musa::Extension::DynamicProxy::DynamicProxy.new(dsl_context)
|
|
89
349
|
def bind=(bind)
|
|
90
350
|
raise 'Already binded' if @bind
|
|
91
351
|
|
|
@@ -102,6 +362,28 @@ module Musa
|
|
|
102
362
|
end
|
|
103
363
|
end
|
|
104
364
|
|
|
365
|
+
# Stops the REPL server and cleans up all threads.
|
|
366
|
+
#
|
|
367
|
+
# This method terminates both the main server thread and all client threads,
|
|
368
|
+
# ensuring a clean shutdown. It's safe to call even if the server is already
|
|
369
|
+
# stopped.
|
|
370
|
+
#
|
|
371
|
+
# ## Shutdown Process
|
|
372
|
+
#
|
|
373
|
+
# 1. Sets run flag to false (stops accepting new connections)
|
|
374
|
+
# 2. Terminates main server thread
|
|
375
|
+
# 3. Terminates all client threads
|
|
376
|
+
# 4. Clears thread tracking
|
|
377
|
+
#
|
|
378
|
+
# @return [void]
|
|
379
|
+
#
|
|
380
|
+
# @note After stopping, the REPL cannot be restarted (would need new instance)
|
|
381
|
+
# @note Uses Thread.pass to ensure thread scheduling
|
|
382
|
+
#
|
|
383
|
+
# @example
|
|
384
|
+
# repl = REPL.new(bind: context)
|
|
385
|
+
# # ... later
|
|
386
|
+
# repl.stop # Clean shutdown
|
|
105
387
|
def stop
|
|
106
388
|
@run = false
|
|
107
389
|
|
|
@@ -114,6 +396,43 @@ module Musa
|
|
|
114
396
|
@client_threads.clear
|
|
115
397
|
end
|
|
116
398
|
|
|
399
|
+
# Sends messages to the connected REPL client.
|
|
400
|
+
#
|
|
401
|
+
# This method allows code running in the REPL to send output back to the
|
|
402
|
+
# client (editor). It's designed to be called from within evaluated code,
|
|
403
|
+
# typically as a replacement for standard Kernel#puts.
|
|
404
|
+
#
|
|
405
|
+
# ## Behavior
|
|
406
|
+
#
|
|
407
|
+
# - If client is connected: sends all messages via TCP
|
|
408
|
+
# - If no client connected: logs warning and ignores messages
|
|
409
|
+
# - Always returns nil (like Kernel#puts)
|
|
410
|
+
#
|
|
411
|
+
# ## Use in DSL Context
|
|
412
|
+
#
|
|
413
|
+
# The DSL context can override Kernel#puts to redirect output to the client:
|
|
414
|
+
#
|
|
415
|
+
# def puts(*args)
|
|
416
|
+
# repl.puts(*args)
|
|
417
|
+
# end
|
|
418
|
+
#
|
|
419
|
+
# This allows code like `puts "Debug: #{value}"` to appear in the editor.
|
|
420
|
+
#
|
|
421
|
+
# @param messages [Array<Object>] messages to send (converted to strings)
|
|
422
|
+
# @return [nil] always returns nil like Kernel#puts
|
|
423
|
+
#
|
|
424
|
+
# @note Thread-safe for multi-threaded code execution
|
|
425
|
+
# @note Messages sent via {#send} with proper escaping
|
|
426
|
+
#
|
|
427
|
+
# @example From evaluated code
|
|
428
|
+
# # In REPL-evaluated code:
|
|
429
|
+
# puts "Starting sequence..."
|
|
430
|
+
# sequencer.at 4 { puts "Bar 4!" }
|
|
431
|
+
# # Output appears in editor
|
|
432
|
+
#
|
|
433
|
+
# @example Multiple messages
|
|
434
|
+
# repl.puts("Line 1", "Line 2", "Line 3")
|
|
435
|
+
# # Sends three separate lines to client
|
|
117
436
|
def puts(*messages)
|
|
118
437
|
if @connection
|
|
119
438
|
messages.each do |message|
|
|
@@ -130,12 +449,80 @@ module Musa
|
|
|
130
449
|
|
|
131
450
|
private
|
|
132
451
|
|
|
452
|
+
# Sends code echo to the client.
|
|
453
|
+
#
|
|
454
|
+
# After receiving and before executing a code block, the REPL echoes it back
|
|
455
|
+
# to the client. This allows the editor to confirm what code will be executed
|
|
456
|
+
# and potentially display it differently from the original input.
|
|
457
|
+
#
|
|
458
|
+
# ## Protocol Format
|
|
459
|
+
#
|
|
460
|
+
# //echo
|
|
461
|
+
# <source code lines>
|
|
462
|
+
# //end
|
|
463
|
+
#
|
|
464
|
+
# @param e [String] source code to echo
|
|
465
|
+
# @param output [TCPSocket] client connection to send to
|
|
466
|
+
# @return [void]
|
|
467
|
+
#
|
|
468
|
+
# @api private
|
|
133
469
|
def send_echo(e, output:)
|
|
134
470
|
send output: output, command: '//echo'
|
|
135
471
|
send output: output, content: e
|
|
136
472
|
send output: output, command: '//end'
|
|
137
473
|
end
|
|
138
474
|
|
|
475
|
+
# Sends formatted exception information to the client.
|
|
476
|
+
#
|
|
477
|
+
# When code execution fails, this method formats the exception with context
|
|
478
|
+
# and sends it to the client in a structured format. The formatting varies
|
|
479
|
+
# based on the exception type and whether it occurred in REPL-executed code.
|
|
480
|
+
#
|
|
481
|
+
# ## Protocol Format
|
|
482
|
+
#
|
|
483
|
+
# //error
|
|
484
|
+
# <error context and message>
|
|
485
|
+
# //backtrace
|
|
486
|
+
# <backtrace lines>
|
|
487
|
+
# <blank line>
|
|
488
|
+
# //end
|
|
489
|
+
#
|
|
490
|
+
# ## Error Formatting Strategies
|
|
491
|
+
#
|
|
492
|
+
# ### ScriptError (SyntaxError, etc.)
|
|
493
|
+
# - Class name
|
|
494
|
+
# - Message (contains syntax details)
|
|
495
|
+
# - No source context (parser-level error)
|
|
496
|
+
#
|
|
497
|
+
# ### Errors outside REPL code
|
|
498
|
+
# - "ClassName: message"
|
|
499
|
+
# - First backtrace location (likely in library)
|
|
500
|
+
#
|
|
501
|
+
# ### Errors in REPL code (most common)
|
|
502
|
+
# - Source context: 3 lines (before, error, after)
|
|
503
|
+
# - Line numbers for orientation
|
|
504
|
+
# - Error marker: "<<< ERROR !!!"
|
|
505
|
+
# - Exception class and message
|
|
506
|
+
# - Filtered backtrace (only REPL-executed code)
|
|
507
|
+
#
|
|
508
|
+
# ## Source Context Example
|
|
509
|
+
#
|
|
510
|
+
# ***
|
|
511
|
+
# [5] some_variable = 42
|
|
512
|
+
# [6] result = divide_by_zero() <<< ERROR !!!
|
|
513
|
+
# [7] puts result
|
|
514
|
+
# ***
|
|
515
|
+
# ZeroDivisionError
|
|
516
|
+
# divided by 0
|
|
517
|
+
#
|
|
518
|
+
# @param e [Exception] the exception to format and send
|
|
519
|
+
# @param output [TCPSocket] client connection to send to
|
|
520
|
+
# @return [void]
|
|
521
|
+
#
|
|
522
|
+
# @note Also logs the full exception to the logger
|
|
523
|
+
# @note Backtrace is filtered to show only '(repl)' locations
|
|
524
|
+
#
|
|
525
|
+
# @api private
|
|
139
526
|
def send_exception(e, output:)
|
|
140
527
|
|
|
141
528
|
@logger.error('REPL') { e.full_message(highlight: @highlight_exception, order: :top) }
|
|
@@ -179,11 +566,47 @@ module Musa
|
|
|
179
566
|
send output: output, command: '//end'
|
|
180
567
|
end
|
|
181
568
|
|
|
569
|
+
# Sends content and/or command to the client.
|
|
570
|
+
#
|
|
571
|
+
# Low-level method for sending data over TCP to the client. Handles
|
|
572
|
+
# optional content (with escaping) and protocol commands.
|
|
573
|
+
#
|
|
574
|
+
# @param output [TCPSocket] client connection to send to
|
|
575
|
+
# @param content [String, nil] text content to send (will be escaped)
|
|
576
|
+
# @param command [String, nil] protocol command to send (e.g., '//echo')
|
|
577
|
+
# @return [void]
|
|
578
|
+
#
|
|
579
|
+
# @note Content is escaped via {#escape} to handle lines starting with '//'
|
|
580
|
+
# @note Commands are sent unmodified
|
|
581
|
+
#
|
|
582
|
+
# @api private
|
|
182
583
|
def send(output:, content: nil, command: nil)
|
|
183
584
|
output.puts escape(content) if content
|
|
184
585
|
output.puts command if command
|
|
185
586
|
end
|
|
186
587
|
|
|
588
|
+
# Escapes text lines that start with '//' to prevent protocol confusion.
|
|
589
|
+
#
|
|
590
|
+
# Since the REPL protocol uses lines starting with '//' as commands
|
|
591
|
+
# (//echo, //error, //end, etc.), any user content that starts with '//'
|
|
592
|
+
# must be escaped by doubling the slashes.
|
|
593
|
+
#
|
|
594
|
+
# ## Escaping Rule
|
|
595
|
+
#
|
|
596
|
+
# - `"//something"` → `"///something"` (escaped)
|
|
597
|
+
# - `"normal text"` → `"normal text"` (unchanged)
|
|
598
|
+
#
|
|
599
|
+
# The client is responsible for unescaping by removing one '//' prefix
|
|
600
|
+
# from any line starting with '///' (assuming it's not a known command).
|
|
601
|
+
#
|
|
602
|
+
# @param text [String] text to potentially escape
|
|
603
|
+
# @return [String] escaped text (or original if no escaping needed)
|
|
604
|
+
#
|
|
605
|
+
# @example
|
|
606
|
+
# escape("//comment") # => "///comment"
|
|
607
|
+
# escape("Hello") # => "Hello"
|
|
608
|
+
#
|
|
609
|
+
# @api private
|
|
187
610
|
def escape(text)
|
|
188
611
|
if text.start_with? '//'
|
|
189
612
|
"//#{text}"
|
|
@@ -193,11 +616,138 @@ module Musa
|
|
|
193
616
|
end
|
|
194
617
|
end
|
|
195
618
|
|
|
619
|
+
# Mixin for DSL contexts that can be bound to a REPL.
|
|
620
|
+
#
|
|
621
|
+
# This module provides the interface required for a DSL context to work
|
|
622
|
+
# with the REPL server. Classes that include this module can execute
|
|
623
|
+
# REPL-sent code in their own context, making their DSL methods available
|
|
624
|
+
# to live coding clients.
|
|
625
|
+
#
|
|
626
|
+
# ## Requirements
|
|
627
|
+
#
|
|
628
|
+
# Classes that include this module must implement the {#binder} method,
|
|
629
|
+
# which should return a Ruby Binding object representing the execution context.
|
|
630
|
+
#
|
|
631
|
+
# ## Integration with DynamicProxy
|
|
632
|
+
#
|
|
633
|
+
# Typically used with `Musa::Extension::DynamicProxy::DynamicProxy`:
|
|
634
|
+
#
|
|
635
|
+
# class MyDSL
|
|
636
|
+
# include CustomizableDSLContext
|
|
637
|
+
#
|
|
638
|
+
# def initialize
|
|
639
|
+
# @repl = REPL.new(bind: Musa::Extension::DynamicProxy::DynamicProxy.new(self))
|
|
640
|
+
# end
|
|
641
|
+
#
|
|
642
|
+
# protected def binder
|
|
643
|
+
# @__binder ||= binding
|
|
644
|
+
# end
|
|
645
|
+
#
|
|
646
|
+
# # DSL methods available in REPL:
|
|
647
|
+
# def play(note)
|
|
648
|
+
# # ...
|
|
649
|
+
# end
|
|
650
|
+
# end
|
|
651
|
+
#
|
|
652
|
+
# ## Execution Context
|
|
653
|
+
#
|
|
654
|
+
# Code sent via REPL is evaluated in the binding returned by {#binder},
|
|
655
|
+
# giving it access to:
|
|
656
|
+
#
|
|
657
|
+
# - Instance variables of the DSL context
|
|
658
|
+
# - All DSL methods (public and private)
|
|
659
|
+
# - Local variables captured in the binding
|
|
660
|
+
#
|
|
661
|
+
# @example Basic implementation
|
|
662
|
+
# class LiveCodingEnvironment
|
|
663
|
+
# include Musa::REPL::CustomizableDSLContext
|
|
664
|
+
#
|
|
665
|
+
# def initialize
|
|
666
|
+
# @sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
667
|
+
# @repl = REPL.new(bind: Musa::Extension::DynamicProxy::DynamicProxy.new(self))
|
|
668
|
+
# end
|
|
669
|
+
#
|
|
670
|
+
# protected def binder
|
|
671
|
+
# @__binder ||= binding
|
|
672
|
+
# end
|
|
673
|
+
#
|
|
674
|
+
# # DSL methods accessible from REPL:
|
|
675
|
+
# def at(position, &block)
|
|
676
|
+
# @sequencer.at(position, &block)
|
|
677
|
+
# end
|
|
678
|
+
# end
|
|
679
|
+
#
|
|
680
|
+
# @example From REPL client
|
|
681
|
+
# # Code sent by editor:
|
|
682
|
+
# at 0 { play C4 }
|
|
683
|
+
# at 4 { play D4 }
|
|
684
|
+
# # Executes in LiveCodingEnvironment instance context
|
|
685
|
+
#
|
|
686
|
+
# @see REPL The REPL server that uses this interface
|
|
687
|
+
# @see Musa::Extension::DynamicProxy::DynamicProxy Wraps contexts for transparent method access
|
|
196
688
|
module CustomizableDSLContext
|
|
689
|
+
# Returns the binding for code execution.
|
|
690
|
+
#
|
|
691
|
+
# Subclasses must implement this method to provide a Ruby Binding object
|
|
692
|
+
# in which REPL code will be evaluated. The binding determines what variables,
|
|
693
|
+
# methods, and constants are accessible to executed code.
|
|
694
|
+
#
|
|
695
|
+
# ## Implementation Pattern
|
|
696
|
+
#
|
|
697
|
+
# The recommended pattern is to cache the binding in an instance variable:
|
|
698
|
+
#
|
|
699
|
+
# protected def binder
|
|
700
|
+
# @__binder ||= binding
|
|
701
|
+
# end
|
|
702
|
+
#
|
|
703
|
+
# This captures the binding at the point of first call, including the
|
|
704
|
+
# instance context (`self`) and any local variables in scope.
|
|
705
|
+
#
|
|
706
|
+
# @return [Binding] the execution context binding
|
|
707
|
+
# @raise [NotImplementedError] if not implemented by including class
|
|
708
|
+
#
|
|
709
|
+
# @note Protected visibility prevents external access while allowing REPL use
|
|
710
|
+
#
|
|
711
|
+
# @example
|
|
712
|
+
# class MyDSL
|
|
713
|
+
# include CustomizableDSLContext
|
|
714
|
+
#
|
|
715
|
+
# protected def binder
|
|
716
|
+
# @__binder ||= binding
|
|
717
|
+
# end
|
|
718
|
+
# end
|
|
197
719
|
protected def binder
|
|
198
720
|
raise NotImplementedError, 'Binder method should be implemented in target namespace as def binder; @__binder ||= binding; end'
|
|
199
721
|
end
|
|
200
722
|
|
|
723
|
+
# Executes source code in the DSL context.
|
|
724
|
+
#
|
|
725
|
+
# Called by the REPL to evaluate received code blocks. Delegates to
|
|
726
|
+
# Binding#eval with the binding provided by {#binder}.
|
|
727
|
+
#
|
|
728
|
+
# ## Parameters
|
|
729
|
+
#
|
|
730
|
+
# - **source_block**: Ruby code as string
|
|
731
|
+
# - **file**: Filename for error reporting (typically '(repl)')
|
|
732
|
+
# - **line**: Starting line number for error reporting
|
|
733
|
+
#
|
|
734
|
+
# ## Error Handling
|
|
735
|
+
#
|
|
736
|
+
# Exceptions raised during evaluation propagate to the caller (REPL),
|
|
737
|
+
# which formats them and sends them to the client.
|
|
738
|
+
#
|
|
739
|
+
# @param source_block [String] Ruby code to execute
|
|
740
|
+
# @param file [String] filename for backtrace (usually '(repl)')
|
|
741
|
+
# @param line [Integer] starting line number for backtrace
|
|
742
|
+
# @return [Object] result of evaluating the source code
|
|
743
|
+
#
|
|
744
|
+
# @raise [Exception] any exception raised by the executed code
|
|
745
|
+
#
|
|
746
|
+
# @example Internal usage by REPL
|
|
747
|
+
# # REPL calls this internally:
|
|
748
|
+
# context.execute("sequencer.at 0 { puts 'tick' }", "(repl)", 1)
|
|
749
|
+
#
|
|
750
|
+
# @see REPL#bind= Where this method is called during code execution
|
|
201
751
|
def execute(source_block, file, line)
|
|
202
752
|
binder.eval source_block, file, line
|
|
203
753
|
end
|