├── .gitignore ├── History.md ├── LICENSE ├── README.md ├── examples ├── find.pl ├── hello.pl └── throw.pl ├── pack.pl ├── prolog ├── jolog.pl ├── jolog │ └── manager.pl └── readme.txt └── t └── parse_join_clause.pl /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | jolog-*.tgz 4 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.0.3 / 2014-11-14 2 | ================== 3 | 4 | * Fix botched release 5 | 6 | 0.0.2 / 2014-11-14 7 | ================== 8 | 9 | * Stop leaking queues, threads and channel messages (Thanks Samer Abdallah) 10 | 11 | 0.0.1 / 2013-08-30 12 | ================== 13 | 14 | * First public release 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Synopsis 2 | 3 | :- use_module(library(jolog)). 4 | % Prolog runs this predicate automatically 5 | main :- 6 | writeln('Starting Jolog'), 7 | % start Jolog, block until it's done 8 | start_jolog(user). 9 | 10 | % Jolog runs this pattern when it starts 11 | main &- 12 | writeln('In Jolog main'), 13 | then, 14 | ( send(hello) % in one parallel process 15 | & sleep(1), % in another parallel process 16 | send(world) 17 | ). 18 | 19 | % Once there are messages on both the 'hello' and 'world' channels 20 | world, hello &- 21 | writeln('Hello, World!'). 22 | 23 | which generates the following output with a small delay between the 24 | second and third lines. 25 | 26 | Starting Jolog 27 | In Jolog main 28 | Hello, World! 29 | 30 | # Description 31 | 32 | WARNING: As suggested by the version number, this is early alpha software. 33 | APIs and behavior are likely to change. 34 | 35 | Jolog is an implementation of join patterns for Prolog that's 36 | [inspired by JoCaml](http://jocaml.inria.fr/). Join patterns provide 37 | a clean, powerful way of thinking about concurrent and parallel 38 | programming. 39 | 40 | Jolog clauses are defined using the &-/2 operator. Use it just like 41 | the `:-/2` operator. The clause's head contains a join pattern. The 42 | body contains processes and optional guards. 43 | 44 | ## Join Patterns 45 | 46 | A join pattern is typically a conjunction (using `,/2`) of smaller 47 | patterns. Each small pattern is a term. The term's name indicates a 48 | channel. The term's arguments indicate the content of a message on 49 | that channel. As you'd expect, patterns are matched via standard 50 | Prolog unification. 51 | 52 | So if our Jolog system has the following outstanding messages: 53 | 54 | * `temperature(75)` 55 | * `humidity(50)` 56 | * `air(overcast, windy)` 57 | 58 | then all the following join patterns would match 59 | 60 | * `temperature(75)` 61 | * `temperature(T)` - binds `T` to `75` 62 | * `air(overcast,windy), humidity(H)` - binds `H` to `50` 63 | 64 | ## Guards 65 | 66 | If a Jolog clause body contains the goal `then/0`, all goals occuring 67 | before `then/0` are considered guards. The guards typically examine 68 | those bindings created by the head's join patterns. All guard goals 69 | must succeed before the matched messages are consumed. 70 | 71 | For example, imagine we're programming a vending machine. The 72 | predicate `price(+Item, -Cost)` tells us how much it costs to purchase 73 | a particular item. The channel `balance/1` receives messages 74 | indicating how much money has been deposited into the vending machine. 75 | The channel `selected/1` receives a message when a customer presses an 76 | item selection button on the vending machine. We only want to 77 | dispense an item if the balance exceeds the selected item's price. 78 | 79 | balance(Bal), selected(Item) &- 80 | price(Item, Cost), 81 | Bal >= Cost, 82 | then, 83 | ... 84 | 85 | Before the elided code (`...`) starts executing, the `balance/1` and 86 | `selected/1` messages are consumed and no longer available to other 87 | Jolog clauses. 88 | 89 | If there is no `then/0` goal, it's the same as if the guard had been 90 | `true`. 91 | 92 | ## Processes 93 | 94 | Processes are concurrent computations. They're executed after a join 95 | pattern matches and all associated guards succeed. They're 96 | inexpensive to create, so don't worry about making thousands of them 97 | if you need to. 98 | 99 | A process can be any code that'd you pass to `call/1`. The simplest 100 | process is just a goal. For example, in the `world, hello` pattern in 101 | the Synopsis above, the process is `writeln('Hello, World!')`. That 102 | goal is executed concurrently and the enclosing Jolog clause doesn't 103 | wait for it to complete. 104 | 105 | It's often convenient to create multiple processes from a single Jolog 106 | clause. That's done with the &/2 operator. Use this operator just 107 | as you would use `;/2`. For example the `main` Jolog clause in the 108 | Synopsis creates two parallel processes. 109 | 110 | Additional [examples are 111 | available](https://github.com/mndrix/jolog/tree/master/examples). 112 | 113 | ## Messages 114 | 115 | Messages are consumed via join patterns (described above). They're 116 | created by calling `send/1`. You can send any term as a message, but 117 | you'll get a warning if no Jolog clauses are listening for that 118 | message. 119 | 120 | # Changes in this Version 121 | 122 | See History.md file. 123 | 124 | # Installation 125 | 126 | Using SWI-Prolog 6.3 or later: 127 | 128 | ?- pack_install(jolog). 129 | 130 | This module uses [semantic versioning](http://semver.org/). 131 | 132 | Source code available and pull requests accepted at 133 | http://github.com/mndrix/jolog 134 | -------------------------------------------------------------------------------- /examples/find.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pl 2 | :- use_module(library(jolog)). 3 | :- use_module(library(apply), [maplist/2]). 4 | :- use_module(library(lambda)). 5 | :- use_module(library(filesex), [directory_file_path/3]). 6 | %:- debug(jolog). 7 | 8 | main(Args) :- 9 | ( Args=[Start|_] -> true ; Start='.' ), 10 | start_jolog(user, entry(dir,Start)). 11 | 12 | 13 | % don't descend into these directories 14 | ignore_directory('.'). 15 | ignore_directory('..'). 16 | ignore_directory('.git'). 17 | 18 | % don't list these files 19 | ignore_file(File) :- % hidden files 20 | atom_concat('.',_,File). 21 | ignore_file(File) :- % backup files 22 | atom_concat(_,'~',File). 23 | 24 | 25 | % Jolog definitions below here 26 | 27 | % generic dir entry whose type we don't yet know 28 | entry(Entry) &- 29 | ( exists_directory(Entry) -> % a directory 30 | file_base_name(Entry, Base), 31 | \+ ignore_directory(Base), 32 | send(entry(dir,Entry)) 33 | ; true -> % a file 34 | send(entry(file,Entry)) 35 | ). 36 | 37 | % directories 38 | entry(dir, Dir) &- 39 | directory_files(Dir, Entries), 40 | maplist(Dir+\E^( 41 | directory_file_path(Dir,E,Path), 42 | send(entry(Path)) 43 | ),Entries). 44 | 45 | % plain files 46 | entry(file, File) &- 47 | file_base_name(File, Base), 48 | \+ ignore_file(Base), 49 | writeln(File). 50 | -------------------------------------------------------------------------------- /examples/hello.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pl 2 | :- use_module(library(jolog)). 3 | %:- debug(jolog). 4 | 5 | % Prolog runs this predicate automatically 6 | main :- 7 | writeln('Starting jolog'), 8 | % start Jolog, block until it's done 9 | start_jolog(user). 10 | 11 | % Jolog runs this pattern on start 12 | main &- 13 | writeln('In jolog main'), 14 | then, 15 | ( send(hello) % one parallel process 16 | & sleep(1), % another parallel process 17 | send(world) 18 | ). 19 | 20 | % Once there are messages on both the 'hello' and 'world' channels 21 | world, hello &- 22 | writeln('Hello, World!'). 23 | -------------------------------------------------------------------------------- /examples/throw.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pl 2 | :- use_module(library(jolog)). 3 | %:- debug(jolog). 4 | 5 | % Prolog runs this predicate automatically 6 | main :- 7 | writeln('Starting jolog'), 8 | % start Jolog, block until it's done 9 | start_jolog(user). 10 | 11 | % Jolog runs this pattern on start 12 | main &- 13 | writeln('In jolog main'), 14 | then, 15 | throw(oh_no_i_died). 16 | -------------------------------------------------------------------------------- /pack.pl: -------------------------------------------------------------------------------- 1 | name(jolog). 2 | title('Concurrency via join calculus'). 3 | 4 | version('0.0.3'). 5 | download('https://github.com/mndrix/jolog/archive/v0.0.3.zip'). 6 | 7 | author('Michael Hendricks','michael@ndrix.org'). 8 | packager('Michael Hendricks','michael@ndrix.org'). 9 | maintainer('Michael Hendricks','michael@ndrix.org'). 10 | home('http://packs.ndrix.com/jolog/index.html'). 11 | -------------------------------------------------------------------------------- /prolog/jolog.pl: -------------------------------------------------------------------------------- 1 | :- module(jolog,[ op(1200,xfx,&-) 2 | , op(1100,xfy,&) 3 | , jolog_import_sentinel/0 4 | , send/1 5 | , start_jolog/1 6 | , start_jolog/2 7 | ]). 8 | :- use_module(library(debug), [debug/3]). 9 | :- use_module(library(list_util), [split/3, xfy_list/3]). 10 | :- use_module(library(lists), [same_length/2]). 11 | :- use_module(library(error), [domain_error/2]). 12 | 13 | :- use_module(library(jolog/manager)). 14 | 15 | 16 | :- thread_local channels/2. % channels(Module, Message) 17 | 18 | /************* Jolog runtime code *******************/ 19 | 20 | %% start_jolog(+Module,+Main) is det 21 | % 22 | % Start running Jolog code defined in Module. After 23 | % creating the runtime environment, this predicate sends the Main 24 | % message. Use that message to trigger the rest 25 | % of your application. For example, 26 | % 27 | % main :- % starting point for Prolog 28 | % start_jolog(user, go). 29 | % go &- % starting point for Jolog 30 | % ( one_process 31 | % & another_process 32 | % ). 33 | % ... 34 | % 35 | % start_jolog/2 returns when one of the following is true: 36 | % 37 | % * a message arrives on the halt/0 channel 38 | % * no join patterns match and no further progress can be made 39 | start_jolog(Module,Main) :- 40 | setup_call_cleanup( 41 | manager_create(Module,Main), 42 | manager_loop(Module), 43 | manager_destroy(Module) 44 | ). 45 | 46 | create_worker(Module,Queue,ThreadId) :- 47 | thread_create(worker_loop(Module,Queue), ThreadId, [detached(true)]). 48 | 49 | 50 | %% start_jolog(+Module) is det. 51 | % 52 | % Like start_jolog/2 using `main` as the first message. 53 | start_jolog(Module) :- 54 | start_jolog(Module, main). 55 | 56 | 57 | %% set_meta(+Module, +Name, +Value) is det. 58 | % 59 | % Set a named meta value for a Jolog runtime defined in Module. 60 | set_meta(Module, Name, Value) :- 61 | retractall(meta(Module,Name,_)), 62 | assertz(meta(Module,Name,Value)). 63 | 64 | 65 | %% meta(+Module, ?Name, ?Value) 66 | % 67 | % True if Jolog runtime defined in Module has a named meta value with 68 | % Name and Value. 69 | :- dynamic meta/3. 70 | 71 | 72 | % called by macro-expanded Jolog code to spawn a new, parallel process. 73 | % Should only be called by the manager thread. 74 | spawn_process(Module, Process) :- 75 | debug(jolog, '~w', spawn_process(Module, Process)), 76 | 77 | % put process code in the workers' queue 78 | meta(Module, work_queue, WorkQueue), 79 | thread_send_message(WorkQueue, run_process(Process)), 80 | 81 | % notify the manager that one more worker is active 82 | meta(Module, manager_queue, ManagerQueue), 83 | thread_send_message(ManagerQueue, active(+1)). 84 | 85 | 86 | %% send(+Message) is det 87 | % 88 | % Sends a Jolog message to the relevant channel. Message should be a 89 | % ground term whose functor indicates the channel and whose arguments 90 | % indicate the message. For example, the following are legitimate 91 | % messages: 92 | % 93 | % send(hello) % hello/0 channel 94 | % send(hello(world)) % hello/1 channel 95 | % send(foo(alpha,beta,gamma,delta)) % foo/4 channel 96 | :- meta_predicate send(:). 97 | send(Module:Message) :- 98 | % someone listens on this channel; send the message 99 | functor(Message, Name, Arity), 100 | defined_channel(Module, Name, Arity), 101 | !, 102 | debug(jolog, '~w', [send(Module,Message)]), 103 | meta(Module, manager_queue, ManagerQueue), 104 | thread_send_message(ManagerQueue, send_message(Message)). 105 | send(Module:Message) :- 106 | % nobody listens on this channel; generate a warning 107 | print_message(warning, jolog_nobody_listening(Module, Message)). 108 | 109 | % loop executed by Jolog worker threads 110 | worker_loop(Module, Queue) :- 111 | debug(jolog,'~w',[worker(waiting)]), 112 | thread_get_message(Queue, Work), 113 | debug(jolog,'~w', [worker(job(Work))]), 114 | ( Work = halt -> 115 | debug(jolog,'worker exiting',[]), 116 | thread_exit(halt) 117 | ; Work = run_process(Goal) -> 118 | catch(Module:ignore(Goal),Ex,report_exception(Module,Goal,Ex)), 119 | meta(Module, manager_queue, ManagerQueue), 120 | debug(jolog,'~w',[worker(finished)]), 121 | thread_send_message(ManagerQueue, active(-1)) 122 | ; % otherwise -> 123 | domain_error(jolog_worker_message, Work) 124 | ), 125 | worker_loop(Module, Queue). 126 | 127 | 128 | report_exception(Module,Goal,Ex) :- 129 | print_message(warning,jolog_worker_crashed(Module,Goal)), 130 | print_message(warning,Ex). 131 | 132 | 133 | prolog:message(jolog_worker_crashed(Module,Goal)) --> 134 | ["Caught exception in Jolog worker running ~q."-[Module:Goal]]. 135 | 136 | 137 | /*************************** Macro expansion code ***********************/ 138 | 139 | %% jolog_import_sentinel 140 | % 141 | % Nothing to see here. This is a junk predicate to keep Jolog macro 142 | % expansion isolated to those modules that want it. 143 | jolog_import_sentinel. 144 | 145 | 146 | % True if the currently loading module wants jolog macro expansion 147 | wants_jolog_expansion :- 148 | prolog_load_context(module, Module), 149 | predicate_property(Module:jolog_import_sentinel, imported_from(jolog)). 150 | 151 | 152 | % Parse a jolog clause into its constituent parts 153 | parse_join_clause((Head &- Body), Patterns, Guards, Processes) :- 154 | % separate head into individual join patterns 155 | xfy_list(',', Head, Patterns), 156 | 157 | % separate body into guards and process terms 158 | xfy_list(',', Body, Goals), 159 | split(Goals, then, BodyParts), 160 | ( BodyParts = [Guards, ProcessTerms] -> 161 | true 162 | ; BodyParts = [ProcessTerms] -> % missing 'then' goal 163 | Guards = [] 164 | ), 165 | 166 | % build process goals from process terms 167 | ( ProcessTerms = [] -> 168 | Processes = [] 169 | ; ProcessTerms = [ProcessDisjunction] -> 170 | xfy_list('&', ProcessDisjunction, Processes) 171 | ; % otherwise -> 172 | xfy_list(',', Process, ProcessTerms), 173 | Processes = [Process] 174 | ). 175 | 176 | 177 | %% build_peek_goal(+Pattern,-MessageRef,-PeekGoal) 178 | % 179 | % Convert a jolog join pattern into a goal which checks whether the 180 | % pattern matches (PeekGoal). If calling PeekGoal succeeds, it binds 181 | % MessageRef to a reference to a clause which represents the matching 182 | % message. This reference can be used with erase/1 to consume the 183 | % message. 184 | build_peek_goal(Module, Pattern, MessageRef, PeekGoal) :- 185 | PeekGoal = jolog:clause(channels(Module,Pattern), true, MessageRef). 186 | 187 | 188 | %% remember_channel(+Module,+JoinPattern) 189 | % 190 | % Makes a note that Module has a join pattern which refers to a 191 | % specific channel. 192 | :- dynamic defined_channel/3. 193 | remember_channel(Module, Pattern) :- 194 | functor(Pattern, Name, Arity), 195 | ( defined_channel(Module,Name,Arity) -> 196 | true 197 | ; % otherwise -> 198 | assertz(defined_channel(Module,Name,Arity)) 199 | ). 200 | 201 | 202 | user:term_expansion((Head &- Body), ('$jolog_code' :- Goals)) :- 203 | wants_jolog_expansion, 204 | parse_join_clause((Head &- Body), Patterns, Guards, Processes), 205 | 206 | % build goals to peek at messages 207 | same_length(Patterns, MessageRefs), 208 | prolog_load_context(module, Module), 209 | maplist(build_peek_goal(Module), Patterns, MessageRefs, Peeks), 210 | 211 | % remember which channels have been defined 212 | prolog_load_context(module, Module), 213 | maplist(remember_channel(Module), Patterns), 214 | 215 | % build jolog clause body 216 | xfy_list(',', PeekGoals, Peeks), 217 | ( Guards=[] -> GuardGoals=true; xfy_list(',', GuardGoals, Guards) ), 218 | Module:dynamic('$jolog_code'/0), 219 | Goals = ( 220 | debug(jolog,'Does head match? ~w', [Head]), 221 | PeekGoals, 222 | GuardGoals, 223 | !, 224 | maplist(erase, MessageRefs), 225 | maplist(jolog:spawn_process(Module), Processes) 226 | ). 227 | user:term_expansion(end_of_file, _) :- 228 | % create Jolog clause to handle system halt 229 | wants_jolog_expansion, 230 | prolog_load_context(module, Module), 231 | 232 | % only add a 'halt' rule if there are other rules 233 | Module:once(clause('$jolog_code', _)), 234 | 235 | term_expansion(( 236 | halt &- 237 | debug(jolog, 'halting', []), 238 | jolog:meta(Module, manager_queue, ManagerQueue), 239 | thread_send_message(ManagerQueue, halt), 240 | then 241 | ), Clause), 242 | Module:asserta(Clause), % halt clause goes first 243 | remember_channel(Module, halt), 244 | 245 | fail. % let others have a chance to expand end_of_file 246 | -------------------------------------------------------------------------------- /prolog/jolog/manager.pl: -------------------------------------------------------------------------------- 1 | :- module(jolog_manager, [ manager_create/2 2 | , manager_destroy/1 3 | , manager_loop/1 4 | ]). 5 | 6 | manager_loop(Module) :- 7 | manager_loop(Module, 0). 8 | 9 | manager_loop(Module, Outstanding) :- 10 | iterate_patterns(Module, Outstanding). 11 | 12 | 13 | % create a Jolog manager 14 | manager_create(Module,StartupMessage) :- 15 | message_queue_create(ManagerQueue), 16 | jolog:set_meta(Module, manager_queue, ManagerQueue), 17 | 18 | message_queue_create(WorkQueue), 19 | jolog:set_meta(Module, work_queue, WorkQueue), 20 | 21 | current_prolog_flag(cpu_count,CoreCount), 22 | WorkerCount is 2*CoreCount, 23 | jolog:set_meta(Module, worker_count, WorkerCount), 24 | length(Workers,WorkerCount), 25 | debug(jolog,"Jolog: starting ~d worker threads.",[WorkerCount]), 26 | maplist(jolog:create_worker(Module,WorkQueue),Workers), 27 | jolog:set_meta(Module, workers, Workers), 28 | 29 | Module:send(StartupMessage). 30 | 31 | 32 | % destroy a Jolog manager created with manager_create/2 33 | manager_destroy(Module) :- 34 | jolog:meta(Module,workers,Workers), 35 | maplist(destroy_worker,Workers), 36 | 37 | jolog:meta(Module,work_queue,WorkQueue), 38 | message_queue_destroy(WorkQueue), 39 | 40 | jolog:meta(Module,manager_queue,ManagerQueue), 41 | message_queue_destroy(ManagerQueue), 42 | 43 | % don't leave messages or metadata lying around 44 | retractall(jolog:meta(Module,_,_)), 45 | retractall(jolog:channels(_,_)). 46 | 47 | destroy_worker(ThreadId) :- 48 | debug(jolog,'Signaling worker ~d to stop',[ThreadId]), 49 | thread_signal(ThreadId,thread_exit(halt)). 50 | 51 | 52 | % match as many join patterns as possible 53 | iterate_patterns(Module, Outstanding) :- 54 | debug(jolog, '~w', [manager(iterate_patterns,Outstanding)]), 55 | Module:'$jolog_code', 56 | !, 57 | iterate_patterns(Module, Outstanding). 58 | iterate_patterns(Module, Outstanding) :- 59 | iterate_events(Module, Outstanding). 60 | 61 | 62 | % process as many manager events as possible 63 | iterate_events(Module, Outstanding) :- 64 | take_event_no_block(Module,Event), 65 | debug(jolog,'~w',[manager(event, Event)]), 66 | handle_event(Event, Module, Outstanding). 67 | 68 | handle_event(send_message(Msg), Module, Outstanding) :- 69 | debug(jolog,"Sending message: ~w",[Msg]), 70 | jolog:assert(channels(Module,Msg)), 71 | iterate_patterns(Module, Outstanding). % patterns might match now 72 | handle_event(active(N), Module, Outstanding0) :- 73 | Outstanding is Outstanding0 + N, 74 | debug(jolog,"Outstanding workers: ~d -> ~d",[Outstanding0,Outstanding]), 75 | iterate_events(Module, Outstanding). 76 | handle_event(none, Module, Outstanding) :- 77 | ( Outstanding > 0 -> % block until pending workers are done 78 | debug(jolog, 'manager blocking', []), 79 | take_event_block(Module,Event), 80 | handle_event(Event, Module, Outstanding) 81 | ; true -> % no chance of forward progress; stop Jolog 82 | debug(jolog,"No events, no outstanding workers: sending halt",[]), 83 | handle_event(send_message(halt), Module, Outstanding) 84 | ). 85 | handle_event(halt, _, _). % no more recursion 86 | 87 | 88 | % Takes the next event that's available for the manager thread. 89 | % Blocks if there are no events available. Should only be called by 90 | % the manager thread. 91 | take_event_block(Module,Event) :- 92 | jolog:meta(Module,manager_queue,ManagerQueue), 93 | thread_get_message(ManagerQueue, Event). 94 | 95 | 96 | % Like take_event_block but binds Event to 'none' instead of blocking. 97 | % Should only be called by the manager thread. 98 | take_event_no_block(Module,Event) :- 99 | jolog:meta(Module,manager_queue,ManagerQueue), 100 | ( thread_get_message(ManagerQueue, Message, [timeout(0)]) -> 101 | Event = Message 102 | ; % otherwise -> 103 | Event = none 104 | ). 105 | -------------------------------------------------------------------------------- /prolog/readme.txt: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /t/parse_join_clause.pl: -------------------------------------------------------------------------------- 1 | :- use_module(library(jolog)). 2 | 3 | :- use_module(library(tap)). 4 | 5 | % simplest possible join clause: one pattern, no guards, trivial process 6 | jolog:parse_join_clause( 7 | ( 8 | pattern_only &- true 9 | ), 10 | [pattern_only], 11 | [], 12 | [true] 13 | ). 14 | 15 | % typical join clause with some of everything 16 | jolog:parse_join_clause( 17 | ( 18 | foo, bar(X) &- 19 | guard_one, 20 | guard_two(X), 21 | then, 22 | ( process_one(X) 23 | & process_two 24 | ) 25 | ), 26 | [foo,bar(X)], 27 | [guard_one, guard_two(X)], 28 | [process_one(X), process_two] 29 | ). 30 | 31 | % join clause with guards but no processes 32 | jolog:parse_join_clause( 33 | ( 34 | channel &- guard_me, then 35 | ), 36 | [channel], 37 | [guard_me], 38 | [] 39 | ). 40 | --------------------------------------------------------------------------------