/*  MAIN_LOOP.PL  */
 
 
:- module main_loop.
 
 
:- public read_and_do_sentences/0,
          process_sentence/2,
          token_error_details/3.
 
 
/*
SPECIFICATION
-------------
 
PUBLIC read_and_do_sentences/0:
 
read_and_do_sentences/0 reads the student's input, parsing and obeying
her sentences, until either she types the BYE command, or a fatal error
occurs. It is called from 'go', to start the Tutor. If your system can
catch break-ins, you may also want to call it after a break-in has
happened.
 
 
PUBLIC process_sentence( NextSentence+, ItsTerminator+ ):
 
Parses and obeys one sentence. The only place this is needed outside
this module is in the screen editor.
 
 
PUBLIC token_error_details( Message, Input, OKTokens ):
 
Calling this just after a tokenisation error will yield the message,
input, and OK tokens. The only place this is needed outside the Tutor
is also in the screen editor.
*/
 
 
/*
IMPLEMENTATION
--------------
 
read_and_do_sentences/0 calls read_and_do_sentences/1. The latter
predicate takes a list as input. This is the list of tokens (if any)
left after the last sentence was extracted. Since
read_and_do_sentences/0 is called under the assumption that there was no
previous input, it passes an empty list to read_and_do_sentences/1.
 
read_and_do_sentences/1 first calls extract_sentence. This takes the
left over tokens, and tries to get a complete sentence from them. If it
can't, it reads more lines until it can (i.e. until it can find a
terminator). Any tokens left over from the final line of _this_ sentence
are passed to the next call of read_and_do_sentences/1.
 
Next, process_sentence parses the sentence, getting back a (very simple)
parse tree and an indication of errors. Depending on this, either
process_ok_sentence or process_bad_sentence is called. Each classifies
the sentence as a fact, question, or command, and takes appropriate
action. In these predicates, a "positioned fact" is either an
un-numbered or a numbered fact.
 
If a tokenisation error is detected, process_sentence gets passed a
structure representing the error, including a message and the preceding
(OK) tokens. It appends the token '...' to the OK tokens, and then
parses them, asserting the result as a bad fact. This trick gets the bad
sentence into the textbase so the student can edit it.
 
The predicates obey_command to report_and_add_bad_fact, called from
process_ok_sentence and process_bad_sentence are exported from COMMANDS.
*/
 
 
:- needs
    add_fact / 1,
    answer_question / 1,
    bug / 2,
    chars_to_tokens / 2,
    define_token_error / 1,
    get_prompt / 1,
    is_command / 1,
    is_positioned_fact / 1,
    is_question / 1,
    obey_command / 1,
    parse / 4,
    read_line_as_chars / 1,
    report_and_add_bad_fact / 2,
    report_bad_command / 2,
    report_bad_question / 2,
    report_token_error / 2,
    set_prompt / 1.
 
 
read_and_do_sentences :-
    read_and_do_sentences( [] ).
 
 
read_and_do_sentences( Tokens ) :-
    extract_sentence( Tokens, NextSentence, ItsTerminator, TokensLeft ),
    process_sentence( NextSentence, ItsTerminator ),
    read_and_do_sentences( TokensLeft ).
 
 
process_sentence( _, token_error(Message,Line,OKTokens) ) :-
    !,
    append( OKTokens, ['...'], Sentence ),
    parse( Sentence, '.', Tree, Error ),
    process_bad_sentence( Tree, token_error(Message,Line,OKTokens) ).
 
process_sentence( NextSentence, ItsTerminator ) :-
    parse( NextSentence, ItsTerminator, Tree, Error ),
    (
        Error \= ok
    ->
        process_bad_sentence( Tree, Error )
    ;
        process_ok_sentence( Tree )
    ).
 
 
process_ok_sentence( Tree ) :-
    is_command( Tree ),
    !,
    obey_command(Tree).
 
process_ok_sentence( Tree ) :-
    is_question( Tree ),
    !,
    answer_question(Tree).
 
process_ok_sentence( Tree ) :-
    is_positioned_fact( Tree ),
    !,
    add_fact(Tree).
 
process_ok_sentence( Tree ) :-
    bug( 'process_ok_sentence: bad tree', [Tree] ).
 
 
process_bad_sentence( Tree, Error ) :-
    is_command( Tree ),
    !,
    report_bad_command( Tree, Error ).
 
process_bad_sentence( Tree, Error ) :-
    is_question( Tree ),
    !,
    report_bad_question( Tree, Error ).
 
process_bad_sentence( Tree, Error ) :-
    is_positioned_fact( Tree ),
    !,
    report_and_add_bad_fact( Tree, Error ).
 
process_bad_sentence( Tree, Error ) :-
    bug( 'process_bad_sentence: bad tree', [Tree,Error] ).
 
 
/*
extract_sentence( Tokens+,
                  Sentence-,
                  Terminator-,
                  RestOfLine-
                ):
 
Declaratively speaking, this is true when Sentence is the first complete
sentence (not including its terminator) which starts with the first
token of Tokens; Terminator is the terminator of that sentence.
 
If Sentence continues over more than one line, extract_sentence will
read as many lines as necessary to complete it. RestOfLine will be that
part of Tokens (if no extra lines were read), or the final line
read (if they were), beyond the terminator. So RestOfLine starts the
following sentence.
 
extract_sentence displays a prompt as it reads each line. This is
'*...' if at least one token of Sentence has already been read.
 
However, if no tokens have yet been read (i.e. the student has typed one
or more blank lines), the prompt is just '*'. We don't want to be silly
and tell the student that she has continued nothing.
 
 
extract_sentence/4 calls extract_sentence/5 with an extra argument. This
argument, WhetherEmpty, starts off as 'empty', and is changed to
'not_empty' when at least one token has been read. This enables the
clause that reads a new line to emit the appropriate prompt.
 
 
There are 5 cases to extract_sentence:
 
1) Clause 1. The current line is non-empty and its first token (T) is a
terminator. If so, return [] as the continuation of the sentence; T as
the terminator; all the tokens beyond T as the rest of the current line.
 
2) Clause 2. The current line is non-empty. Add its first token to the
sentence being built and make a recursive call.
 
3) Clause 3. The current line is empty. In that case, we read the next
line as characters, tokenise it, and use the tokens.
 
4) Clause 4. The current line contained an end-of-file. It's hard to
know what to do here. Since (a) we haven't told the student about
control-Z; and (b) it is not a valid terminator anyway; we just ignore
it, and proceed to the next line.
 
5) Clause 5. There was a tokenisation error in the current line. Again,
hard to know what to do. Should we keep on reading until a terminator,
or give up now? I give up now, and return the structure
    token_error(Message,Input,OKTokens)
in place of the sentence terminator. process_sentence gets this and
asserts the OK tokens as a fact, so they can be edited.
*/
 
 
extract_sentence( Tokens, RestOfSentence, T, RestOfLine ) :-
    extract_sentence( Tokens, RestOfSentence, T, RestOfLine, empty ).
 
 
extract_sentence( [atom(T)|Rest], [], T, Rest, _ ) :-
    ( T = '.' ; T = '?' ),
    /*  if T is a sentence terminator  */
    !.
 
extract_sentence( [Token|RestOfTokens], [Token|RestOfSentence],
                  T, RestOfLine, _
                ) :-
    !,
    extract_sentence( RestOfTokens, RestOfSentence, T, RestOfLine,
                      not_empty
                    ).
 
extract_sentence( [], RestOfSentence,
                  T, RestOfLine, WhetherEmpty
                ) :-
    !,
    (
        WhetherEmpty = empty
    ->
        Prompt = '* '
    ;
        Prompt = '*... '
    ),
    get_prompt( OldPrompt ),
    set_prompt( Prompt ),
    read_line_tokens( NextLine ),
    set_prompt( OldPrompt ),
    extract_sentence( NextLine, RestOfSentence, T, RestOfLine, WhetherEmpty ).
 
extract_sentence( end_of_file, RestOfSentence,
                  T, RestOfLine, WhetherEmpty
                ) :-
    !,
    extract_sentence( [], RestOfSentence,
                      T, RestOfLine, WhetherEmpty
                    ).
 
extract_sentence( token_error(M,Line,OKTokens), [line(Line)],
                  token_error(M,Line,OKTokens), [], _
                ) :-
    !.
 
 
/*  read_line_tokens( NextLineAsTokens- ):
        Read and tokenise the next line, returning either a list of
        tokens, end_of_file, or token_error(Message,Input,OKTokens).            
 
        For the meaning of the latter, see TOKENISE.PL.
*/
read_line_tokens( NextLineAsTokens ) :-
    read_line_as_chars( NextLineAsChars ),
    (
        NextLineAsChars = end_of_file
    ->
        NextLineAsTokens = end_of_file
    ;
        tokenise_line( NextLineAsChars, NextLineAsTokens, Status ),
        (
            Status = ok
        ->
            true
        ;
            NextLineAsTokens = Status
        )
    ).
 
 
/*
Syntax errors and line tokenisation.
------------------------------------
 
We catch these by redefining TOKENISE's token-error predicate to
save the message, input, and all the preceding (OK) tokens.
*/
 
 
/*  Tokenise LineAsChars, catching any tokenisation errors.  */
tokenise_line( LineAsChars, LineAsTokens, ok ) :-
    chars_to_tokens( LineAsChars, LineAsTokens ),
    !.
 
tokenise_line( LineAsChars, _, token_error(Message,LineAsChars,OKTokens) ) :-
    token_error_details( Message, _, OKTokens ).
 
 
catch_token_error( Message, Chars, OKTokens ) :-
    asserta( '$users_token_error'(Message,Chars,OKTokens) ).
 
 
token_error_details( Message, Input, OKTokens ) :-
    '$users_token_error'(Message,Input,OKTokens),
    retractall( '$users_token_error'(_,_,_) ).
    /*  Care!!! The screen editor relies on this.  */
 
 
:- define_token_error( catch_token_error ).
 
 
:- endmodule.
