Commit ed7f5e04 authored by CD's avatar CD
Browse files

So I can pull....

parent 14364076
......@@ -2,4 +2,3 @@
set actr_port 2650
set actr_address "localhost"
# Port settings for ACT-R server started at 21:32:19 5/31/2019
# Port settings for ACT-R server started at 21:18:46 6/18/2019
set actr_port 2650
set actr_address "10.8.0.6"
set actr_address "192.168.1.78"
......@@ -15,22 +15,14 @@ if $size_mismatch {
-message "The screen resolution is not the same as it was the last time the Environment was used. Should the window positions reset to the defaults?"]
} else { set reset_window_sizes 0}
if {$reset_window_sizes != "yes"} {
set window_config(.bufferstatus) 554x278+1270+771
set changed_window_list(.bufferstatus) 1
set window_config(.visicon) 1460x359+192+455
set changed_window_list(.visicon) 1
set window_config(.audicon) 870x148+845+620
set window_config(.audicon) 870x150+525+440
set changed_window_list(.audicon) 1
set window_config(.control_panel) 317x700+840+291
set window_config(.visicon) 1920x1001+610+440
set changed_window_list(.visicon) 1
set window_config(.control_panel) 235x700+1665+190
set changed_window_list(.control_panel) 1
set window_config(.declarative) 854x300+1070+570
set changed_window_list(.declarative) 1
set window_config(.whynot) 692x300+827+349
set changed_window_list(.whynot) 1
set window_config(.buffers) 763x240+551+347
set window_config(.buffers) 350x240+785+420
set changed_window_list(.buffers) 1
set window_config(.procedural) 1098x400+645+231
set changed_window_list(.procedural) 1
}
set gui_options(p_selected) #44DA22
set gui_options(p_matched) #FCA31D
......
;;; -*- mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;
;;; Author : Dan Bothell
;;; Copyright : (c) 2016 Dan Bothell
;;; Availability: Covered by the GNU LGPL, see LGPL.txt
;;; Address : Department of Psychology
;;; Address : Department of Psychology
;;; : Carnegie Mellon University
;;; : Pittsburgh, PA 15213-3890
;;; : db30@andrew.cmu.edu
;;;
;;;
;;;
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;
;;; Filename : dispatcher.lisp
;;; Version : 3.1
;;;
;;; Description :
;;;
;;; Bugs :
;;;
;;; Description :
;;;
;;; Bugs :
;;;
;;; To do : [ ] Kill threads that aren't needed instead of just letting them
;;; run to completion.
;;;
;;; run to completion.
;;;
;;; ----- History -----
;;; 2016.11.16 Dan [1.0]
;;; : * Actually put the header on the file and for now have the
......@@ -59,8 +59,8 @@
;;; : * Changed load-act-r-model command to error if already evaluating.
;;; : * Don't attempt to do any fancy encoding/decoding for symbols
;;; : now -- let it use the default camel case stuff for decoding
;;; : because at this point the only thing converted to symbols
;;; : will be the object slot names which have fixed values.
;;; : because at this point the only thing converted to symbols
;;; : will be the object slot names which have fixed values.
;;; : However, the lisp symbol to string needs to not use the
;;; : camel case converter otherwise something like visual-location
;;; : gets converted to "visualLocation" which is bad.
......@@ -70,7 +70,7 @@
;;; : that.
;;; 2016.12.02 Dan
;;; : * Changed how the print-error-message function handles things
;;; : for ACL and CCL to just write the error message because
;;; : for ACL and CCL to just write the error message because
;;; : that seems to work better across different error/condition
;;; : types.
;;; 2016.12.09 Dan
......@@ -87,13 +87,13 @@
;;; : initial colon, a string which starts and ends with a ' will
;;; : be converted into a string without the first and last chars,
;;; : all other strings will be upcased and interned in the package
;;; : stored in *default-package*. Other items which are valid
;;; : values are t, nil, numbers, and symbols which will be left
;;; : stored in *default-package*. Other items which are valid
;;; : values are t, nil, numbers, and symbols which will be left
;;; : unchanged. If a list (or nested lists) of items is provdided
;;; : it will be recursively processed to create a list of items
;;; : as above. If an invalid item is provided or an unprocessable
;;; : item is encountered (a single quote for example since the
;;; : first and last characters are ') then an error will be
;;; : first and last characters are ') then an error will be
;;; : generated.
;;; 2016.12.19 Dan
;;; : * The feature test for ACL is allegro not acl -- fixed that
......@@ -112,7 +112,7 @@
;;; : match the client and unfortunately the client version is in
;;; : a "better" order for users...
;;; : * Updated the process-external-input function to better handle
;;; : the distinction between an invalid command method and an
;;; : the distinction between an invalid command method and an
;;; : error in the parameters provided.
;;; 2017.02.08 Dan
;;; : * Return-result better handles when it can't encode the return
......@@ -150,7 +150,7 @@
;;; : off explicitly to the next available one instead of using
;;; : a condition variable to let 'anyone' grab it. Then it is
;;; : up to the worker to put itself back into the available list
;;; : upon completion. There are actually two sets of workers
;;; : upon completion. There are actually two sets of workers
;;; : needed -- those for handling the 'action' in the thread that's
;;; : running process-external-input for a connection and those for
;;; : evaluating a command as needed in the perform-action function
......@@ -193,10 +193,10 @@
;;; : * Make print-warning thread safe.
;;; 2017.06.22 Dan
;;; : * Make get-model safe by protecting meta-p-models.
;;; 2017.06.28 Dan
;;; 2017.06.28 Dan
;;; : * Have load-act-r-model go through the dispatcher.
;;; 2017.07.13 Dan
;;; : * Add the general-trace and act-r-output to support output
;;; : * Add the general-trace and act-r-output to support output
;;; : that can happen without a model defined, and update the echo
;;; : and suppress functions to deal with it too.
;;; : * Moved the print-error-message function to misc-utils since
......@@ -222,7 +222,7 @@
;;; : method is given.
;;; : * Allow an added command to have no function and when evaluated
;;; : it doesn't call anything. That is purely for monitoring
;;; : purposes and simplifies things like model-trace needing a
;;; : purposes and simplifies things like model-trace needing a
;;; : dummy function and should improve performance.
;;; 2017.08.25 Dan
;;; : * Load-act-r-command now returns t if the load succeeded.
......@@ -307,7 +307,7 @@
;;; : but fixed that now.
;;; 2018.04.05 Dan [2.1]
;;; : * For the standalone, if it fails to open the default port it
;;; : will try incrementing until it can. Once it does it will
;;; : will try incrementing until it can. Once it does it will
;;; : print the port used and write that number to a file in the
;;; : current home directory: ~/act-r-port-num.txt.
;;; : * Loading from sources will also increment port until success
......@@ -328,7 +328,7 @@
;;; : when checking for local connection.
;;; 2018.06.07 Dan
;;; : * Added encode-string-names which is the recursive version of
;;; : encode-string.
;;; : encode-string.
;;; 2018.06.11 Dan
;;; : * Have decode-string-names use decode-string for consistency.
;;; 2018.06.12 Dan
......@@ -344,7 +344,7 @@
;;; : to return nil not a list of nil when there are no options.
;;; 2018.07.11 Dan
;;; : * In addition to writing out the port and address in the home
;;; : directory write out a config file for the environment
;;; : directory write out a config file for the environment
;;; : explicitly to override the defaults for when it can't read
;;; : the files (not sure why that happens, but seems to be an
;;; : issue on one of the Summer School student's machines).
......@@ -383,14 +383,14 @@
;;; 2019.02.15 Dan
;;; : * Added a switch to force it to use the local address, and
;;; : going to set that in the standalones because public wifi
;;; : networks often block machine-to-machine which can also
;;; : networks often block machine-to-machine which can also
;;; : block machine to itself. So, have the standalones default
;;; : to that since external access is likely going to be rarely
;;; : used.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; General Docs:
;;;
;;;
;;; This code provides a new central command dispatch system which has a TCP/IP
;;; server listening on 127.0.0.1:2650 and will accept connections for remote
;;; access to that command dispatch system. There are 7 operations available
......@@ -402,7 +402,7 @@
;;; add: Specify the name of a command to be added to those available through the
;;; central dispatch system. Also requires specifying the name which gets
;;; sent to the command owner for handling it (it doesn't have to match the
;;; command name provided to other systems). Optionally, can provide a
;;; command name provided to other systems). Optionally, can provide a
;;; string which documents the command, a flag to indicate that only one
;;; instance of the command should be evaluated at a time, and a name for
;;; a Lisp side macro to call the command.
......@@ -423,7 +423,7 @@
;;; the monitor is called after, but is just passed the parameter list
;;; that the original command received. That is now the default if neither
;;; :before or :after isn't specified.
;;; remove-monitor: Stop the monitoring of a command set up with the monitor
;;; remove-monitor: Stop the monitoring of a command set up with the monitor
;;; operation. Requires the monitored command and the monitoring
;;; command to remove from it.
;;; list-commands: Takes no parameters and returns a list of lists. Each of the
......@@ -436,8 +436,8 @@
;;; if it is a reserved name). If it is not a command it returns the
;;; single value nil.
;;; set-name: Takes one parameter and returns one value. If the parameter is
;;; a string then that string is recorded as the current name of the
;;; connection (does not have to be unique) and t is returned. Otherwise
;;; a string then that string is recorded as the current name of the
;;; connection (does not have to be unique) and t is returned. Otherwise
;;; it returns nil.
;;; list-connections: Takes no parameters. Returns a list of lists where each
;;; sublist represents a connection to the dispatcher and contains 5 items:
......@@ -459,11 +459,11 @@
;;; list-act-r-commands ()
;;; check-act-r-command(name)
;;; list-act-r-connections()
;;;
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Design Choices:
;;;
;;;
;;; The main idea is that everything must pass through the central command dispatch
;;; system so that from a high-level view it doesn't matter how/where a particular
;;; component is implemented. At this point, that's not fully realized because
......@@ -481,7 +481,7 @@
;;; remote system to provide both roles to really do anything which seems to
;;; be more complicated than it needs to be. However, could change that if I
;;; find that existing libraries for other languages handle that cleanly.
;;;
;;;
;;; All errors/problems write to *error-output*, but may want some other sort of
;;; error logging eventually. Everything should be wrapped with enough handler-case
;;; and unwind-protect code to keep it from crashing while handling the external
......@@ -493,13 +493,13 @@
;;; - a thread monitoring the action queue which spawns a new thread for each
;;; action that needs to be processed
;;;
;;; This code is peppered with enough locks to keep things thread safe, but
;;; This code is peppered with enough locks to keep things thread safe, but
;;; the command code which gets evaluated needs to protect itself as well. Just
;;; specifying a single instance might be enough, but it should be careful of
;;; dependencies if it calls other commands too.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;
;;; The code
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
......@@ -522,7 +522,7 @@
(or (cdr (assoc name (meta-p-models mp)))
(and (stringp name) (cdr (assoc (string->name name) (meta-p-models mp))))))))
;;; The main control
;;; The main control
(defstruct dispatcher
connection-interface
......@@ -561,26 +561,26 @@
(defvar *dispatch-command* 0)
(defstruct dispatch-command
name documentation
underlying-function
evaluator
name documentation
underlying-function
evaluator
local-name
;; these are lists of dispacth-command structures
before
after
before
after
simple
monitoring
;;;
single-instance
(lock (bt:make-lock (concatenate 'string "dispatch-command-lock " (princ-to-string (incf *dispatch-command*))))))
(defvar *pr-lock* 0)
(defvar *pr-cv* 0)
(defstruct pending-request id action (lock (bt:make-lock (concatenate 'string "pending-request " (princ-to-string (incf *pr-lock*)))))
(cv (bt:make-condition-variable :name (concatenate 'string "pending-request-cv " (princ-to-string (incf *pr-cv*)))))
(defstruct pending-request id action (lock (bt:make-lock (concatenate 'string "pending-request " (princ-to-string (incf *pr-lock*)))))
(cv (bt:make-condition-variable :name (concatenate 'string "pending-request-cv " (princ-to-string (incf *pr-cv*)))))
complete success result)
(defvar *handler-lock* 0)
......@@ -640,13 +640,13 @@ and the result is written as String."
(defun add-act-r-command (name &optional function documentation (single-instance t)(local-name nil))
(let ((a (make-action :type 'add-command
(let ((a (make-action :type 'add-command
:parameters (list :name name :underlying-function function
:documentation documentation
:documentation documentation
:evaluator :lisp
:single-instance single-instance
:local-name local-name))))
(multiple-value-bind (success params)
(send-des-action a)
(if (listp params)
......@@ -655,7 +655,7 @@ and the result is written as String."
(defun remove-act-r-command (name)
(let ((a (make-action :type 'remove-command
(let ((a (make-action :type 'remove-command
:parameters (list name)
:evaluator :lisp)))
(multiple-value-bind (success params)
......@@ -663,7 +663,7 @@ and the result is written as String."
(if (listp params)
(values-list (cons success params))
(values-list (list success params))))))
(defun evaluate-act-r-command (name &rest rest)
(let ((a (make-action :type 'execute-command
:model (current-model-struct)
......@@ -740,7 +740,7 @@ and the result is written as String."
(bt:with-lock-held ((handler-received-requests-lock handler))
(setf p (find a (handler-received-requests handler) :key 'pending-request-action))
(when p (setf (handler-received-requests handler) (remove p (handler-received-requests handler)))))
(if p
(when (pending-request-id p)
(let (res json-result)
......@@ -751,7 +751,7 @@ and the result is written as String."
(format nil "{\"result\": ~a, \"error\": null, \"id\": ~s}~c" res (pending-request-id p) (code-char 4))
(format nil "{\"result\": null, \"error\": {\"message\": ~a}, \"id\": ~s}~c" res (pending-request-id p) (code-char 4)))))
(error ()
(setf json-result (format nil "{\"result\": null, \"error\": {\"message\": ~s}, \"id\": ~s}~c"
(setf json-result (format nil "{\"result\": null, \"error\": {\"message\": ~s}, \"id\": ~s}~c"
(format nil "Could not convert result ~s to JSON" result) (pending-request-id p) (code-char 4)))))
(handler-case
(bt:with-lock-held ((handler-stream-lock handler))
......@@ -767,27 +767,27 @@ and the result is written as String."
;; details already filled in just create the internal controls
(setf (action-cv a) (bt:make-condition-variable :name (concatenate 'string "action-cv " (princ-to-string (incf *action-locks*)))))
(setf (action-lock a) (bt:make-lock (concatenate 'string "action-lock " (princ-to-string *action-locks*))))
(bt:acquire-lock (action-lock a) t)
(add-action-to-queue a)
(handler-case
(handler-case
(progn
(loop
(when (action-complete a)
(return))
(bt:condition-wait (action-cv a) (action-lock a)))
(values (action-result-success a) (action-result a)))
(error (x) (let ((string (format nil "Error ~/print-error-message/ occurred while waiting for action ~s ~s." x (action-type a) (action-parameters a))))
(send-error-output string)
(values nil string)))
(condition (x) (let ((string (format nil "Condition ~/print-error-message/ occurred while waiting for action ~s ~s." x (action-type a) (action-parameters a))))
(send-error-output string)
(values nil string)))))
(defun add-action-to-queue (action)
(bt:with-lock-held ((dispatcher-action-lock *dispatcher*))
(push-last action (dispatcher-action-queue *dispatcher*))
......@@ -796,7 +796,7 @@ and the result is written as String."
(defun dispatcher-process-actions (dispatcher)
(bt:acquire-lock (dispatcher-action-lock dispatcher) t)
(loop
(loop
(dolist (x (dispatcher-action-queue dispatcher))
(handler-case
(perform-action dispatcher x)
......@@ -829,7 +829,7 @@ and the result is written as String."
;; show the 'real' address and a true external couldn't get
;; in anyway.
(setf *allow-external-connections* t)
(if local-host-ip
(progn
(setf *server-host* local-host-ip)
......@@ -841,12 +841,12 @@ and the result is written as String."
(let ((s (handler-case (usocket:socket-listen host remote-port)
(usocket:address-in-use-error () (progn
(send-error-output "Could not open a socket for host ~s port ~s because it is already open~%" host remote-port)
(incf remote-port)
(incf remote-port)
:again))
(error (x) (progn
(send-error-output "Error ~/print-error-message/ occurred while trying to open a socket for host ~s port ~s~%" x host remote-port)
nil)))))
(cond ((null s)
(cond ((null s)
(format t "Server not started.~%")
(return-from start-des nil))
((eq s :again))
......@@ -857,16 +857,16 @@ and the result is written as String."
(progn
(if create
(setf *dispatcher*
(make-dispatcher
(make-dispatcher
:connection-socket server-socket
:action-lock (bt:make-lock "dispatcher-action-lock")
:action-lock (bt:make-lock "dispatcher-action-lock")
:handler-lock (bt:make-lock "dispatcher-handler-lock")
:command-lock (bt:make-lock "dispatcher-command-lock")
:action-cv (bt:make-condition-variable :name "dispatcher-action-condition-variable")
:available-received-lock (bt:make-lock "dispatcher-available-received-lock")
:available-execute-lock (bt:make-lock "dispatcher-available-execute-lock")))
(setf (dispatcher-connection-socket *dispatcher*) server-socket))
(setf (dispatcher-connection-interface *dispatcher*)
(bt:make-thread (lambda ()
(let ((*package* *default-package*))
......@@ -874,23 +874,23 @@ and the result is written as String."
(let ((new (usocket:socket-accept server-socket)))
(initialize-dispatch-connection *dispatcher* new)))))
:name "initialize-dispatch-connection"))
(setf (dispatcher-action-thread *dispatcher*)
(bt:make-thread (lambda ()
(let ((*package* *default-package*))
(dispatcher-process-actions *dispatcher*)))
:name "dispatcher-process-actions"))
(handler-case (with-open-file (f "~/act-r-address.txt" :direction :output :if-exists :supersede :if-does-not-exist :create)
(format f "~a" host))
(error (x)
(send-error-output "Error ~/print-error-message/ occurred while trying to write the address to ~s~%" x (translate-logical-pathname "~/act-r-address.txt"))))
(handler-case (with-open-file (f "~/act-r-port-num.txt" :direction :output :if-exists :supersede :if-does-not-exist :create)
(format f "~d" remote-port))
(error (x)
(send-error-output "Error ~/print-error-message/ occurred while trying to write the port number to ~s~%" x (translate-logical-pathname "~/act-r-port-num.txt"))))
(handler-case (with-open-file (f "ACT-R:environment;GUI;init;05-current-net.tcl" :direction :output :if-exists :supersede :if-does-not-exist :create)
(multiple-value-bind
(second minute hour date month year)
......@@ -899,18 +899,18 @@ and the result is written as String."
hour minute second month date year remote-port host)))
(error (x)
(send-error-output "Error ~/print-error-message/ occurred while trying to write the Environment network config file ~s~%" x (translate-logical-pathname "ACT-R:environment;GUI;init;05-current-net.tcl"))))
t)
nil)))
(defun dont-start-des ()
(setf *dispatcher*
(make-dispatcher
(make-dispatcher
:connection-socket nil
:action-lock (bt:make-lock "dispatcher-action-lock")
:action-lock (bt:make-lock "dispatcher-action-lock")
:handler-lock (bt:make-lock "dispatcher-handler-lock")
:command-lock (bt:make-lock "dispatcher-command-lock")
:action-cv (bt:make-condition-variable :name "dispatcher-action-condition-variable")
......@@ -921,7 +921,7 @@ and the result is written as String."
(let ((*package* *default-package*))
(dispatcher-process-actions *dispatcher*)))
:name "dispatcher-process-actions")))
(defun init-des ()
(start-des nil))
......@@ -951,81 +951,81 @@ and the result is written as String."
;;; Char code 4 terminates a message in and out
(defun initialize-dispatch-connection (dispatcher socket)
(if (and (null *allow-external-connections*)
(not (or (usocket:ip= (usocket::get-peer-address socket) *server-host*)
(usocket:ip= (usocket::get-peer-address socket) #(127 0 0 1)))))
(send-error-output "Attempted connection from ~s was denied~%" (usocket::vector-quad-to-dotted-quad (usocket::get-peer-address socket)))
(let ((handler (make-handler :socket socket)))
(setf (handler-thread handler) (bt:make-thread (lambda ()
(let ((*package* *default-package*))
(unwind-protect
(process-external-input handler (usocket:socket-stream socket))
(handler-case
(progn
(handler-case
(progn
;; Things we've received and not initiated should be removed
(dolist (x (handler-commands handler))
(remove-command dispatcher (dispatch-command-name x)))
;; things we've sent but haven't received need to return an error result
(bt:with-lock-held ((handler-sent-requests-lock handler))
(dolist (x (handler-sent-requests handler))
(abort-request x)))
;; things we've received but haven't returned should probably have their
;; threads killed, but for now just going to let them run out naturally
;; and fail when trying to return results
(bt:with-lock-held ((dispatcher-handler-lock dispatcher))
(setf (dispatcher-connections dispatcher)
(remove handler (dispatcher-connections dispatcher))))
(usocket:socket-close socket))
(error (x) (send-error-output "Error ~/print-error-message/ encountered while cleaning up a terminated connection." x))))))
:name "process-external-input"))
(bt:with-lock-held ((dispatcher-handler-lock dispatcher))
(push handler (dispatcher-connections dispatcher))))))
(defun received-worker (worker)
(bt:acquire-lock (worker-lock worker) t)
(bt:with-lock-held ((worker-start-lock worker))
(bt:condition-notify (worker-started worker)))
(loop
(bt:condition-wait (worker-cv worker) (worker-lock worker))
(multiple-value-bind (success result)
(send-des-action (worker-action worker))
(return-result (worker-handler worker) (worker-action worker) success result))
(bt:with-lock-held ((dispatcher-available-received-lock *dispatcher*))
(push-last worker (dispatcher-available-received-workers *dispatcher*)))))
(defun execute-worker (worker)
(bt:acquire-lock (worker-lock worker) t)
(bt:with-lock-held ((worker-start-lock worker))
(bt:condition-notify (worker-started worker)))
(loop
(bt:condition-wait (worker-cv worker) (worker-lock worker))
(let* ((*package* *default-package*)
(others (worker-other worker))
(c (first others))
(parameters (second others)))
(execute-command (worker-action worker) c parameters))
(bt:with-lock-held ((dispatcher-available-execute-lock *dispatcher*))
(push-last worker (dispatcher-available-execute-workers *dispatcher*)))))
......@@ -1042,7 +1042,7 @@ and the result is written as String."
(error ()
(send-error-output "External connection terminated while trying to read data.~%")
(return-from process-external-input)))))
(cond ((eq char :done)
(send-error-output "External connection closed.")
(return))
......@@ -1058,7 +1058,7 @@ and the result is written as String."
(assoc :id message))
(let* ((m (cdr (assoc :method message)))
(params (cdr (assoc :params message)))
(action (cond
(action (cond
((and (string-equal m "add")
(>= (length params) 2)
(stringp (first params))
......@@ -1083,15 +1083,15 @@ and the result is written as String."
(stringp (first params)))
(make-action :type 'execute-command
:model (second params)
:parameters (list :name (first params)
:parameters (list :name (first params)
:parameters (cddr params))
:evaluator handler))
((and (string-equal m "monitor")
(>= (length params) 2)
(stringp (first params))
(stringp (second params)))
(make-action :type (if (and (stringp (third params)) (string-equal (third params) "after"))