;;; Sly ;;; Copyright (C) 2013, 2014 David Thompson ;;; ;;; This program is free software: you can redistribute it and/or ;;; modify it under the terms of the GNU General Public License as ;;; published by the Free Software Foundation, either version 3 of the ;;; License, or (at your option) any later version. ;;; ;;; This program is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;;; General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with this program. If not, see ;;; . ;;; Commentary: ;; ;; Deferred procedure scheduling. ;; ;;; Code: (define-module (sly agenda) #:use-module (ice-9 q) #:use-module (srfi srfi-1) #:use-module (srfi srfi-9) #:use-module (srfi srfi-26) #:use-module (sly coroutine) #:export (make-agenda agenda? agenda-time current-agenda with-agenda agenda-tick! agenda-clear! schedule schedule-interval schedule-each wait)) ;; This code is a modified version of the agenda implementation in ;; SICP. Thank you, SICP! ;;; ;;; Time segment ;;; (define-record-type (%make-time-segment time queue) time-segment? (time segment-time) (queue segment-queue)) (define (make-time-segment time . callbacks) "Create a new time segment at TIME and enqueues everything in the list CALLBACKS." (let ((segment (%make-time-segment time (make-q)))) ;; Enqueue all callbacks (for-each (cut segment-enq segment <>) callbacks) segment)) (define (segment-enq segment callback) "Add the CALLBACK procedure to SEGMENT's queue." (enq! (segment-queue segment) callback)) ;;; ;;; Agenda ;;; (define-record-type (%make-agenda time segments) agenda? (time %agenda-time set-agenda-time!) (segments agenda-segments set-agenda-segments!)) (define (make-agenda) "Create a new, empty agenda." (%make-agenda 0 '())) (define (agenda-empty? agenda) "Return #t if AGENDA has no scheduled procedures." (null? (agenda-segments agenda))) (define (first-segment agenda) "Return the first time segment in AGENDA." (car (agenda-segments agenda))) (define (rest-segments agenda) "Return everything but the first segment in AGENDA." (cdr (agenda-segments agenda))) (define (agenda-add-segment agenda time callback) "Add a new time segment to the beginning of AGENDA at the given TIME and enqueue CALLBACK." (set-agenda-segments! agenda (cons (make-time-segment time callback) (agenda-segments agenda)))) (define (insert-segment segments time callback) "Insert a new time segment after the first segment in SEGMENTS." (set-cdr! segments (cons (make-time-segment time callback) (cdr segments)))) (define (first-agenda-item agenda) "Return the first time segment queue in AGENDA." (if (agenda-empty? agenda) (error "Agenda is empty") (segment-queue (first-segment agenda)))) (define (agenda-time-delay agenda dt) "Return the sum of the time delta, DT, and the current time of AGENDA." (+ (%agenda-time agenda) (inexact->exact (round dt)))) (define (flush-queue! q) "Dequeue and execute every member of Q." (unless (q-empty? q) ((deq! q)) ;; Execute scheduled procedure (flush-queue! q))) (define (%agenda-tick! agenda) "Move AGENDA forward in time and run scheduled procedures." (set-agenda-time! agenda (1+ (%agenda-time agenda))) (let next-segment () (unless (agenda-empty? agenda) (let ((segment (first-segment agenda))) ;; Process time segment if it is scheduled before or at the ;; current agenda time. (when (>= (%agenda-time agenda) (segment-time segment)) (flush-queue! (segment-queue segment)) (set-agenda-segments! agenda (rest-segments agenda)) (next-segment)))))) (define (%agenda-clear! agenda) "Remove all scheduled procedures from AGENDA." (set-agenda-segments! agenda '())) (define (%schedule agenda thunk delay) "Schedule THUNK to be run after DELAY ticks of AGENDA." (let ((time (agenda-time-delay agenda delay))) (define (belongs-before? segments) (or (null? segments) (< time (segment-time (car segments))))) (define (add-to-segments segments) ;; Add to existing time segment if the times match (if (= (segment-time (car segments)) time) (segment-enq (car segments) thunk) ;; Continue searching (if (belongs-before? (cdr segments)) ;; Create new time segment and insert it where it belongs (insert-segment segments time thunk) ;; Continue searching (add-to-segments (cdr segments))))) (if (belongs-before? (agenda-segments agenda)) (agenda-add-segment agenda time thunk) (add-to-segments (agenda-segments agenda))) *unspecified*)) (define current-agenda (make-parameter (make-agenda) (lambda (val) (if (agenda? val) val (error "Must be an agenda"))))) (define-syntax-rule (with-agenda agenda body ...) (parameterize ((current-agenda agenda)) body ...)) (define (agenda-time) "Return the time of the current agenda." (%agenda-time (current-agenda))) (define (agenda-tick!) "Increment time for the current agenda and run scheduled procedures." (%agenda-tick! (current-agenda))) (define (agenda-clear!) "Remove all scheduled procedures from the current agenda." (%agenda-clear! (current-agenda))) (define* (schedule thunk #:optional (delay 1)) "Schedule THUNK to be applied after DELAY ticks of the current agenda, or 1 tick if DELAY is not specified." (%schedule (current-agenda) thunk delay)) (define (schedule-interval thunk interval) "Schedule THUNK to be applied every INTERVAL ticks of the current agenda." (coroutine (while #t (wait interval) (thunk)))) (define (schedule-each thunk) "Schedule THUNK to be applied upon every tick of the current agenda." (schedule-interval thunk 1)) (define (wait delay) "Abort coroutine and schedule the continuation to be run after DELAY ticks of the current agenda." (yield (cut schedule <> delay)))