001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.commands3;
006
007import static edu.wpi.first.units.Units.Seconds;
008import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
009
010import edu.wpi.first.units.measure.Time;
011import edu.wpi.first.wpilibj.Timer;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.List;
015import java.util.function.BooleanSupplier;
016import java.util.function.Consumer;
017
018/**
019 * A coroutine object is injected into command's {@link Command#run(Coroutine)} method to allow
020 * commands to yield and compositions to run other commands. Commands are considered <i>bound</i> to
021 * a coroutine while they're scheduled; attempting to use a coroutine outside the command bound to
022 * it will result in an {@code IllegalStateException} being thrown.
023 */
024public final class Coroutine {
025  private final Scheduler m_scheduler;
026  private final Continuation m_backingContinuation;
027
028  /**
029   * Creates a new coroutine. Package-private; only the scheduler should be creating these.
030   *
031   * @param scheduler The scheduler running the coroutine
032   * @param scope The continuation scope the coroutine's backing continuation runs in
033   * @param callback The callback for the continuation to execute when mounted. Often a command
034   *     function's body.
035   */
036  Coroutine(Scheduler scheduler, ContinuationScope scope, Consumer<Coroutine> callback) {
037    m_scheduler = scheduler;
038    m_backingContinuation = new Continuation(scope, () -> callback.accept(this));
039  }
040
041  /**
042   * Yields control back to the scheduler to allow other commands to execute. This can be thought of
043   * as "pausing" the currently executing command.
044   *
045   * @return true
046   * @throws IllegalStateException if called anywhere other than the coroutine's running command
047   */
048  public boolean yield() {
049    requireMounted();
050
051    try {
052      return m_backingContinuation.yield();
053    } catch (IllegalStateException e) {
054      if ("Pinned: MONITOR".equals(e.getMessage())) {
055        // Raised when a continuation yields inside a synchronized block or method:
056        // https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/jdk/internal/vm/Continuation.java#L396-L402
057        // Note: Not a thing in Java 24+
058        // Rethrow with an error message that's more helpful for our users
059        throw new IllegalStateException(
060            "Coroutine.yield() cannot be called inside a synchronized block or method. "
061                + "Consider using a Lock instead of synchronized, "
062                + "or rewrite your code to avoid locks and mutexes altogether.",
063            e);
064      } else {
065        // rethrow
066        throw e;
067      }
068    }
069  }
070
071  /**
072   * Parks the current command. No code in a command declared after calling {@code park()} will be
073   * executed. A parked command will never complete naturally and must be interrupted or canceled.
074   *
075   * @throws IllegalStateException if called anywhere other than the coroutine's running command
076   */
077  @SuppressWarnings("InfiniteLoopStatement")
078  public void park() {
079    requireMounted();
080
081    while (true) {
082      // 'this' is required because 'yield' is a semi-keyword and needs to be qualified
083      this.yield();
084    }
085  }
086
087  /**
088   * Schedules a child command and then immediately returns. The child command will run until its
089   * natural completion, the parent command exits, or the parent command cancels it.
090   *
091   * <p>This is a nonblocking operation. To fork and then wait for the child command to complete,
092   * use {@link #await(Command)}.
093   *
094   * <p>The parent command will continue executing while the child command runs, and can resync with
095   * the child command using {@link #await(Command)}.
096   *
097   * <pre>{@code
098   * Command example() {
099   *   return Command.noRequirements().executing(coroutine -> {
100   *     Command child = ...;
101   *     coroutine.fork(child);
102   *     // ... do more things
103   *     // then sync back up with the child command
104   *     coroutine.await(child);
105   *   }).named("Example");
106   * }
107   * }</pre>
108   *
109   * <p>Note: forking a command that conflicts with a higher-priority command will fail. The forked
110   * command will not be scheduled, and the existing command will continue to run.
111   *
112   * @param commands The commands to fork.
113   * @throws IllegalStateException if called anywhere other than the coroutine's running command
114   * @see #await(Command)
115   */
116  public void fork(Command... commands) {
117    requireMounted();
118
119    requireNonNullParam(commands, "commands", "Coroutine.fork");
120    for (int i = 0; i < commands.length; i++) {
121      requireNonNullParam(commands[i], "commands[" + i + "]", "Coroutine.fork");
122    }
123
124    // Check for user error; there's no reason to fork conflicting commands simultaneously
125    ConflictDetector.throwIfConflicts(List.of(commands));
126
127    // Shorthand; this is handy for user-defined compositions
128    for (var command : commands) {
129      m_scheduler.schedule(command);
130    }
131  }
132
133  /**
134   * Forks off some commands. Each command will run until its natural completion, the parent command
135   * exits, or the parent command cancels it. The parent command will continue executing while the
136   * forked commands run, and can resync with the forked commands using {@link
137   * #awaitAll(Collection)}.
138   *
139   * <pre>{@code
140   * Command example() {
141   *   return Command.noRequirements().executing(coroutine -> {
142   *     Collection<Command> innerCommands = ...;
143   *     coroutine.fork(innerCommands);
144   *     // ... do more things
145   *     // then sync back up with the inner commands
146   *     coroutine.awaitAll(innerCommands);
147   *   }).named("Example");
148   * }
149   * }</pre>
150   *
151   * <p>Note: forking a command that conflicts with a higher-priority command will fail. The forked
152   * command will not be scheduled, and the existing command will continue to run.
153   *
154   * @param commands The commands to fork.
155   * @throws IllegalStateException if called anywhere other than the coroutine's running command
156   */
157  public void fork(Collection<? extends Command> commands) {
158    fork(commands.toArray(Command[]::new));
159  }
160
161  /**
162   * Awaits completion of a command. If the command is not currently scheduled or running, it will
163   * be scheduled automatically. This is a blocking operation and will not return until the command
164   * completes or has been interrupted by another command scheduled by the same parent.
165   *
166   * @param command the command to await
167   * @throws IllegalStateException if called anywhere other than the coroutine's running command
168   * @see #fork(Command...)
169   */
170  public void await(Command command) {
171    requireMounted();
172
173    requireNonNullParam(command, "command", "Coroutine.await");
174
175    m_scheduler.schedule(command);
176
177    while (m_scheduler.isScheduledOrRunning(command)) {
178      // If the command is a one-shot, then the schedule call will completely execute the command.
179      // There would be nothing to await
180      this.yield();
181    }
182  }
183
184  /**
185   * Awaits completion of all given commands. If any command is not current scheduled or running, it
186   * will be scheduled.
187   *
188   * @param commands the commands to await
189   * @throws IllegalArgumentException if any of the commands conflict with each other
190   * @throws IllegalStateException if called anywhere other than the coroutine's running command
191   */
192  public void awaitAll(Collection<? extends Command> commands) {
193    requireMounted();
194
195    requireNonNullParam(commands, "commands", "Coroutine.awaitAll");
196    int i = 0;
197    for (Command command : commands) {
198      requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAll");
199      i++;
200    }
201
202    ConflictDetector.throwIfConflicts(commands);
203
204    for (var command : commands) {
205      m_scheduler.schedule(command);
206    }
207
208    while (commands.stream().anyMatch(m_scheduler::isScheduledOrRunning)) {
209      this.yield();
210    }
211  }
212
213  /**
214   * Awaits completion of all given commands. If any command is not current scheduled or running, it
215   * will be scheduled.
216   *
217   * @param commands the commands to await
218   * @throws IllegalArgumentException if any of the commands conflict with each other
219   * @throws IllegalStateException if called anywhere other than the coroutine's running command
220   */
221  public void awaitAll(Command... commands) {
222    awaitAll(Arrays.asList(commands));
223  }
224
225  /**
226   * Awaits completion of any given commands. Any command that's not already scheduled or running
227   * will be scheduled. After any of the given commands completes, the rest will be canceled.
228   *
229   * @param commands the commands to await
230   * @throws IllegalArgumentException if any of the commands conflict with each other
231   * @throws IllegalStateException if called anywhere other than the coroutine's running command
232   */
233  public void awaitAny(Collection<? extends Command> commands) {
234    requireMounted();
235
236    requireNonNullParam(commands, "commands", "Coroutine.awaitAny");
237    int i = 0;
238    for (Command command : commands) {
239      requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAny");
240      i++;
241    }
242
243    ConflictDetector.throwIfConflicts(commands);
244
245    // Schedule anything that's not already queued or running
246    for (var command : commands) {
247      m_scheduler.schedule(command);
248    }
249
250    while (commands.stream().allMatch(m_scheduler::isScheduledOrRunning)) {
251      this.yield();
252    }
253
254    // At least one command exited; cancel the rest.
255    commands.forEach(m_scheduler::cancel);
256  }
257
258  /**
259   * Awaits completion of any given commands. Any command that's not already scheduled or running
260   * will be scheduled. After any of the given commands completes, the rest will be canceled.
261   *
262   * @param commands the commands to await
263   * @throws IllegalArgumentException if any of the commands conflict with each other
264   * @throws IllegalStateException if called anywhere other than the coroutine's running command
265   */
266  public void awaitAny(Command... commands) {
267    awaitAny(Arrays.asList(commands));
268  }
269
270  /**
271   * Waits for some duration of time to elapse. Returns immediately if the given duration is zero or
272   * negative. Call this within a command or command composition to introduce a simple delay.
273   *
274   * <p>For example, a basic autonomous routine that drives straight for 5 seconds:
275   *
276   * <pre>{@code
277   * Command timedDrive() {
278   *   return drivebase.run(coroutine -> {
279   *     drivebase.tankDrive(1, 1);
280   *     coroutine.wait(Seconds.of(5));
281   *     drivebase.stop();
282   *   }).named("Timed Drive");
283   * }
284   * }</pre>
285   *
286   * <p>Note that the resolution of the wait period is equal to the period at which {@link
287   * Scheduler#run()} is called by the robot program. If using a 20 millisecond update period, the
288   * wait will be rounded up to the nearest 20 millisecond interval; in this scenario, calling
289   * {@code wait(Milliseconds.of(1))} and {@code wait(Milliseconds.of(19))} would have identical
290   * effects.
291   *
292   * <p>Very small loop times near the loop period are sensitive to the order in which commands are
293   * executed. If a command waits for 10 ms at the end of a scheduler cycle, and then all the
294   * commands that ran before it complete or exit, and then the next cycle starts immediately, the
295   * wait will be evaluated at the <i>start</i> of that next cycle, which occurred less than 10 ms
296   * later. Therefore, the wait will see not enough time has passed and only exit after an
297   * additional cycle elapses, adding an unexpected extra 20 ms to the wait time. This becomes less
298   * of a problem with smaller loop periods, as the extra 1-loop delay becomes smaller.
299   *
300   * @param duration the duration of time to wait
301   * @throws IllegalStateException if called anywhere other than the coroutine's running command
302   */
303  public void wait(Time duration) {
304    requireMounted();
305
306    requireNonNullParam(duration, "duration", "Coroutine.wait");
307
308    var timer = new Timer();
309    timer.start();
310    while (!timer.hasElapsed(duration.in(Seconds))) {
311      this.yield();
312    }
313  }
314
315  /**
316   * Yields until a condition is met.
317   *
318   * @param condition The condition to wait for
319   * @throws IllegalStateException if called anywhere other than the coroutine's running command
320   */
321  public void waitUntil(BooleanSupplier condition) {
322    requireMounted();
323
324    requireNonNullParam(condition, "condition", "Coroutine.waitUntil");
325
326    while (!condition.getAsBoolean()) {
327      this.yield();
328    }
329  }
330
331  /**
332   * Advanced users only: this permits access to the backing command scheduler to run custom logic
333   * not provided by the standard coroutine methods. Any commands manually scheduled through this
334   * will be canceled when the parent command exits - it's not possible to "fork" a command that
335   * lives longer than the command that scheduled it.
336   *
337   * @return the command scheduler backing this coroutine
338   * @throws IllegalStateException if called anywhere other than the coroutine's running command
339   */
340  public Scheduler scheduler() {
341    requireMounted();
342
343    return m_scheduler;
344  }
345
346  private boolean isMounted() {
347    return m_backingContinuation.isMounted();
348  }
349
350  private void requireMounted() {
351    // Note: attempting to yield() outside a command will already throw an error due to the
352    // continuation being unmounted, but other actions like forking and awaiting should also
353    // throw errors. For consistent messaging, we use this helper in all places, not just the
354    // ones that interact with the backing continuation.
355
356    if (isMounted()) {
357      return;
358    }
359
360    throw new IllegalStateException("Coroutines can only be used by the command bound to them");
361  }
362
363  // Package-private for interaction with the scheduler.
364  // These functions are not intended for team use.
365
366  void runToYieldPoint() {
367    m_backingContinuation.run();
368  }
369
370  void mount() {
371    Continuation.mountContinuation(m_backingContinuation);
372  }
373
374  boolean isDone() {
375    return m_backingContinuation.isDone();
376  }
377}