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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -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