Exemplo n.º 1
0
def parse_character_line_start(symbols, symidx, characters, stage):
    """
    Parse the start of a character's line (e.g. "Juliet:").
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :param characters: The list of characters in the program.
    :param stage: The characters on stage.
    :returns: symidx, speaker, other character (being spoken to).
    :raises SplError: if there is an error.
    """

    # first a character
    if symbols[symidx][0] != SYM_CHARACTER:
        raise SplError('Expected character to start their line.')
    speaker = symbols[symidx][1]
    if speaker not in characters:
        raise SplError('Unknown character ' + speaker)
    if speaker not in stage:
        raise SplError('Character ' + speaker +
                       ' trying to speak while not on stage.')
    if len(stage) != 2:
        raise SplError(
            'Character ' + speaker +
            ' trying to speak to themselves (or more than 1 person).')
    symidx += 1

    # then a colon
    if symbols[symidx][0] != SYM_COLON:
        raise SplError('Colon expected after character to open their line.')
    symidx += 1

    (spoken_to, ) = stage - {speaker}

    return symidx, speaker, spoken_to
Exemplo n.º 2
0
def parse_header(symbols, symidx, header_symbol, act_counter, scene_counter):
    """
    Parse the act or scene header.
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :param header_symbol: The symbol which constitutes the header (SYM_ACT or SYM_SCENE)
    :param act_counter: The act counter, to make sure the acts are in order/generate the method name.
    :param scene_counter: The scene counter, to make sure the scenes are in order.
    :returns: The name of the act/scene's method, symidx, and act or scene counter.
    :raises SplError: If there is an error.
    """

    counter = act_counter if header_symbol == SYM_ACT else scene_counter

    # first Act or Scene
    if symbols[symidx][0] != header_symbol:
        raise SplError('Expected act or scene keyword.')
    symidx += 1

    # then a Roman numeral
    if symbols[symidx][0] != SYM_ROMAN_NUMERAL:
        raise SplError('Expected Roman numeral after act or scene keyword.')

    # with a valid number according to the counter
    num = symbols[symidx][1]
    if num != counter + 1:
        raise SplError('Act or scene out of order.')
    counter += 1
    symidx += 1

    # then a colon
    if symbols[symidx][0] != SYM_COLON:
        raise SplError('Expected colon after act or scene declaration.')

    # then an arbitrary number of symbols and an END_PUNCTUATION
    try:
        symidx += 1
        while symbols[symidx][0] != SYM_END_PUNCTUATION:
            symidx += 1
    except IndexError:
        raise SplError(
            'Expected end punctuation after act or scene declaration.')

    if header_symbol == SYM_ACT:
        method = 'act' + str(num)
    else:
        method = 'act' + str(act_counter) + 'scene' + str(num)
    return method, symidx + 1, counter
Exemplo n.º 3
0
def skip_as(symbols, symidx):
    """:returns: symidx, 1 after the next 'as'."""
    try:
        symidx += 1
        while symbols[symidx][0] != SYM_AS:
            symidx += 1
        return symidx + 1
    except IndexError:
        raise SplError('No matching "as".')
Exemplo n.º 4
0
def skip_till_end_punct(symbols, symidx):
    """:returns: symidx, 1 after the next end punctuation."""
    try:
        symidx += 1  # past this symbol
        while symbols[symidx][0] != SYM_END_PUNCTUATION:
            symidx += 1
        return symidx + 1
    except IndexError:
        raise SplError(
            'End punctuation expected, but none found till end of program.')
Exemplo n.º 5
0
def validate_line(speaker, spoken_to):
    """
    Raise SplError if a line cannot be spoken right now (i.e. speaker or spoken_to is None).
    :param speaker: The character speaking.
    :param spoken_to: The character being spoken to.
    :raises SplError: if a line cannot be spoken.
    """

    if speaker is None or spoken_to is None:
        raise SplError(
            'A line cannot be spoken because no character is speaking it.')
Exemplo n.º 6
0
def read_characters(symbols, symidx):
    """
    Read the list of characters from the symbols.
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :returns: A list of characters, and the new symidx.
    :raises SplError: if it's in a bad format or there are duplicate characters.
    """

    characters = []

    try:
        while symbols[symidx][0] != SYM_ACT:
            # make sure there's a character
            symidx = next_is(symbols,
                             symidx,
                             SYM_CHARACTER,
                             'Character expected in preamble.',
                             increment=False)

            # make sure it isn't a duplicate
            character = symbols[symidx][1]
            if character in characters:
                raise SplError('Duplicate characters are not allowed.')
            characters.append(character)
            symidx += 1

            # there then has to be a comma
            symidx = next_is(symbols,
                             symidx,
                             SYM_COMMA,
                             'Comma expected after character in preamble.',
                             increment=False)

            # then eventually there has to be an END_PUNCTUATION
            symidx = skip_till_end_punct(symbols, symidx)
    except IndexError:
        # there are no acts
        raise SplError("You can't just have a play with no acts.")

    return characters, symidx
Exemplo n.º 7
0
def parse_jump(symbols, symidx):
    """
    Parse a "jump" - i.e. let us return to scene [X], we must proceed to act [Y], etc.
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :returns: symidx, SYM_ACT (if jumping to act) or SYM_SCENE (if jumping to scene), act/scene number
    :raises SplError: if there is an error.
    """

    # must start with SYM_JUMP
    if symbols[symidx][0] != SYM_JUMP:
        raise SplError(
            'Jump must start with "let us return", "we shall proceed", etc.')
    symidx += 1

    # next "act" or "scene"
    if symbols[symidx][0] not in (SYM_ACT, SYM_SCENE):
        raise SplError(
            'Jump ("let us return" or similar) must be followed with "act" or "scene".'
        )
    jump_type = symbols[symidx][0]
    symidx += 1

    # then a Roman numeral
    if symbols[symidx][0] != SYM_ROMAN_NUMERAL:
        raise SplError(
            'Act or scene keyword in jump ("let us return", etc.) must be followed by Roman numeral.'
        )
    jump_dest = symbols[symidx][1]
    symidx += 1

    # then end punctuation
    if symbols[symidx][0] != SYM_END_PUNCTUATION:
        raise SplError(
            'Expected end punctuation ("." or "!") to end jump ("let us return", etc.).'
        )
    symidx += 1

    return symidx, jump_type, jump_dest
Exemplo n.º 8
0
def parse_expr_operator(fmt_str, symbols, symidx, characters, speaker,
                        spoken_to):
    # e.g. "sum", "difference"
    symidx, expr1 = parse_expression(symbols, symidx + 1, characters, speaker,
                                     spoken_to)

    # there must be "and" separating them
    if symbols[symidx][0] != SYM_AND:
        raise SplError('Expected "and" separating two addends of sum.')

    symidx, expr2 = parse_expression(symbols, symidx + 1, characters, speaker,
                                     spoken_to)
    return symidx, fmt_str.format(expr1, expr2)
Exemplo n.º 9
0
def parse_assignment(symbols, symidx, characters, speaker, spoken_to):
    """
    Parse an assignment - starting with a 2nd person pronoun. E.g. "Thou art as beautiful as a rose".
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :param characters: The list of characters.
    :param speaker: The character speaking.
    :param spoken_to: The character being spoken to (assigned to).
    :returns: symidx, the generated Java code.
    :raises SplError: if there is an error.
    """

    # first: 2nd person pronoun
    if symbols[symidx][0] != SYM_2ND_PERSON_PRONOUN:
        raise SplError(
            'Expected 2nd person pronoun (you, thou, etc.) to start assignment.'
        )
    symidx += 1

    # optional SYM_ASSIGNMENT (art, as, etc.)
    if symbols[symidx][0] == SYM_ASSIGNMENT:
        symidx += 1

    # optional as ... as
    if symbols[symidx][0] == SYM_AS:
        symidx = skip_as(symbols, symidx)

    # expression
    symidx, expr = parse_expression(symbols, symidx, characters, speaker,
                                    spoken_to)

    # then end punctuation
    if symbols[symidx][0] != SYM_END_PUNCTUATION:
        raise SplError('End punctuation expected after assignment.')
    symidx += 1

    java = spoken_to + ' = ' + expr + ';'
    return symidx, java
Exemplo n.º 10
0
def next_is(symbols, symidx, symbol_to_check, msg, increment=True):
    """
    Raise an error if this symbol isn't symbol_to_check.
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :param symbol_to_check: The symbol that this one must be.
    :param msg: The message for the SplError if it is not.
    :param increment: Whether to increment symidx or not.
    :return: symidx, possibly +1
    :raises SplError: if this symbol isn't symbol_to_check.
    """

    if symbols[symidx][0] != symbol_to_check:
        raise SplError(msg)
    return symidx + 1 if increment else symidx
Exemplo n.º 11
0
def translate(spl, java_classname):
    """
    This is the main entry point for actual SPL to Java translation.
    :param spl: The SPL code.
    :param java_classname: The name of the output Java class.
    :returns: The translated Java code.
    :raises SplError: If there is an error in the SPL code.
    """

    tokens = tokenize(spl)
    symbols = symbolize(tokens)

    # filter ignored symbols
    symbols = list(filter(lambda s: s[0] != SYM_IGNORE, symbols))

    if not symbols:
        # the file was empty? or nonsense?
        raise SplError('SPL input was empty or nonsensical.')

    # go past everything up to and including the first SYM_END_PUNCTUATION
    # symidx is the index of the current symbol
    symidx = symbols.index((SYM_END_PUNCTUATION, )) + 1

    # read the list of characters
    characters, symidx = read_characters(symbols, symidx)

    # setup the act and scene counters + stage
    act_counter = 0
    scene_counter = 0
    stage = set()

    # start the Java generation
    java = '''\
// Generated by Ryan Dancy's SPL to Java translator.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Scanner;

public class %s {
\tprivate static Scanner scanner = new Scanner(System.in);
''' % java_classname

    # add the characters
    for character in characters:
        # there's a stack and a number for each character
        java += '\tprivate static int %s;\n' % character
        java += '\tprivate static Deque<Integer> %s_stk = new ArrayDeque<Integer>();\n' % character

    # add the main method
    java += '\tpublic static void main(String[] args) {\n\t\t'

    speaker = None
    spoken_to = None

    # to make sure we don't jump to a nonexistent act/scene
    acts_scenes_jumped_to = set(
    )  # set of (SYM_ACT/SYM_SCENE, act/scene number)

    # to prevent the "unreachable code" error
    last_was_if = False
    need_new_method = False

    # generate the rest of the code
    while symidx != len(symbols):
        symbol = symbols[symidx][0]
        if DEBUG:
            print('Translating: symbol =', symbol, 'symidx =', symidx,
                  'speaker =', speaker, 'spoken_to =', spoken_to)

        if symbol == SYM_ACT or symbol == SYM_SCENE:
            # starting a new act or scene
            method, symidx, counter = parse_header(symbols, symidx, symbol,
                                                   act_counter, scene_counter)

            if symbol == SYM_ACT:
                act_counter = counter
                scene_counter = 0  # reset scene counter
            else:
                scene_counter = counter

            # call the new method from the previous one (if it wouldn't cause an error), then start the new one
            if need_new_method:
                java = java[:-1]  # remove the last tab
            else:
                java += method + '();\n\t'
            java += '}\n\t'
            java += 'private static void ' + method + '() {\n\t\t'

            need_new_method = False

        elif symbol == SYM_OPEN_STAGE_DIRECTION:
            # stage direction
            stage, symidx = parse_stage_direction(symbols, symidx, characters,
                                                  stage)
            speaker = spoken_to = None  # reset speaker and spoken_to so stage directions can't be in middle of line

        elif symbol == SYM_CHARACTER and symbols[symidx + 1][0] == SYM_COLON:
            # character's line start (e.g. "Juliet:")
            if act_counter == 0 or scene_counter == 0:
                raise SplError(
                    'A character cannot speak outside of an act and scene.')
            symidx, speaker, spoken_to = parse_character_line_start(
                symbols, symidx, characters, stage)

        elif symbol == SYM_2ND_PERSON_PRONOUN:
            # assigning to the spoken_to character
            validate_line(speaker, spoken_to)
            symidx, assignment = parse_assignment(symbols, symidx, characters,
                                                  speaker, spoken_to)
            java += assignment + '\n\t\t'

        elif symbol == SYM_ASSIGNMENT:
            # question
            validate_line(speaker, spoken_to)
            symidx, speaker, spoken_to, if_statement = parse_question(
                symbols, symidx, characters, speaker, spoken_to, stage)
            java += if_statement + ' '

        elif symbol == SYM_JUMP:
            # jump to another scene - call the method then return
            validate_line(speaker, spoken_to)
            symidx, act_or_scene, number = parse_jump(symbols, symidx)
            acts_scenes_jumped_to.add((act_or_scene, number))

            # in a block so that it can be used with if statements/questions
            java += '{ %s%d(); return; }\n\t\t' % (
                'act' if act_or_scene == SYM_ACT else 'act%dscene' %
                act_counter, number)

            if not last_was_if:
                need_new_method = True  # it's a definite return

        elif symbol == SYM_PUSH_TO_STACK:
            # push to spoken_to's stack ("remember")
            validate_line(speaker, spoken_to)
            symidx = skip_till_end_punct(symbols, symidx)
            java += '{0}_stk.push({0});\n\t\t'.format(spoken_to)

        elif symbol == SYM_POP_FROM_STACK:
            # pop from spoken_to's stack ("recall")
            validate_line(speaker, spoken_to)
            symidx = skip_till_end_punct(symbols, symidx)
            java += '{0} = {0}_stk.pop();\n\t\t'.format(spoken_to)

        elif symbol == SYM_INPUT_NUMBER:
            # input a number into spoken_to ("listen to your/thy heart")
            validate_line(speaker, spoken_to)
            symidx = next_is(
                symbols, symidx + 1, SYM_END_PUNCTUATION,
                'Expected end punctuation after "listen to your/thy heart".')
            java += '{} = scanner.nextInt();\n\t\t'.format(spoken_to)

        elif symbol == SYM_INPUT_CHARACTER:
            # input a character into spoken_to ("open your/thy mind")
            validate_line(speaker, spoken_to)
            symidx = next_is(
                symbols, symidx + 1, SYM_END_PUNCTUATION,
                'Expected end punctuation after "open your/thy mind".')
            java += '''try {{
\t\t\t{0} = scanner.findInLine(".").charAt(0);
\t\t}} catch (NullPointerException e) {{
\t\t\t{0} = -1;
\t\t}}
\t\t'''.format(spoken_to)

        elif symbol == SYM_OUTPUT_NUMBER:
            # output a number from spoken_to ("open your/thy heart")
            validate_line(speaker, spoken_to)
            symidx = next_is(
                symbols, symidx + 1, SYM_END_PUNCTUATION,
                'Expected end punctuation after "open your/thy heart".')
            java += 'System.out.print({});\n\t\t'.format(spoken_to)

        elif symbol == SYM_OUTPUT_CHARACTER:
            # output a character from spoken_to ("speak your/thy mind")
            validate_line(speaker, spoken_to)
            symidx = next_is(
                symbols, symidx + 1, SYM_END_PUNCTUATION,
                'Expected end punctuation after "speak your/thy mind".')
            java += 'System.out.print((char) {});\n\t\t'.format(spoken_to)

        elif symbol == SYM_END_PUNCTUATION:
            # ignore double punctuation like !!
            symidx += 1  # just skip it

        else:
            # unknown symbol
            raise SplError('Bad symbol at start of line; symbol=' +
                           str(symbol))

        last_was_if = (symbol == SYM_ASSIGNMENT)
        if need_new_method and symbol not in (SYM_JUMP, SYM_END_PUNCTUATION):
            # there was non-act/scene code after an unguarded jump
            raise SplError(
                'A jump unguarded by a question must be the last statement in its act or scene.'
            )

    # validate the acts and scenes jumped to
    for act_or_scene, num in acts_scenes_jumped_to:
        if act_or_scene == SYM_ACT and num > act_counter:
            raise SplError('Jump to nonexistent act ' + num)
        if act_or_scene == SYM_SCENE and num > scene_counter:
            raise SplError('Jump to nonexistent scene ' + num)

    return java[:-1] + '}\n}\n'  # remove last "\t"
Exemplo n.º 12
0
def parse_question(symbols, symidx, characters, speaker, spoken_to, stage):
    """
    Parse a question and the subsequent "if so," and translate into an if statement.
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :param characters: The list of characters.
    :param speaker: The character speaking.
    :param spoken_to: The character being spoken to (assigned to).
    :param stage: The set of characters on stage.
    :returns: symidx, speaker, spoken_to, the generated Java code (this could possibly change
        the speaker/spoken_to if there's a new character line in the middle).
    :raises SplError: if there is an error.
    """

    # a question must start with "is", "art", etc - SYM_ASSIGNMENT
    if symbols[symidx][0] != SYM_ASSIGNMENT:
        raise SplError('"Is", "are", "art", etc. must start a question.')
    symidx += 1

    # expression, then greater than/worse than/as...as, then expression
    symidx, expr1 = parse_expression(symbols, symidx, characters, speaker,
                                     spoken_to)

    if symbols[symidx][0] == SYM_AS:
        symidx = skip_as(symbols, symidx)
        op = '=='
    elif symbols[symidx][0] == SYM_GREATER_THAN:
        if symbols[symidx + 1][0] == SYM_ADJECTIVE:
            symidx += 1  # skip adjective in case of "more"
        op = '>'
        symidx += 1
    elif symbols[symidx][0] == SYM_LESS_THAN:
        if symbols[symidx + 1][0] == SYM_ADJECTIVE:
            symidx += 1  # skip adjective in case of "less"
        op = '<'
        symidx += 1
    else:
        raise SplError(
            'Expression in question must be followed by greater than/less than symbol or as ... as.'
        )

    symidx, expr2 = parse_expression(symbols, symidx, characters, speaker,
                                     spoken_to)

    # then a question mark
    if symbols[symidx][0] != SYM_QUESTION_MARK:
        raise SplError('Question must end with question mark.')
    symidx += 1

    # then possibly a new character
    if symbols[symidx][0] == SYM_CHARACTER:
        symidx, speaker, spoken_to = parse_character_line_start(
            symbols, symidx, characters, stage)

    # then "if so" or "if not" followed by a comma
    if_type = symbols[symidx][0]
    if if_type not in (SYM_IF_SO, SYM_IF_NOT):
        raise SplError('Question must be followed by "if so" or "if not".')
    symidx += 1

    if symbols[symidx][0] != SYM_COMMA:
        raise SplError('"If so" or "if not" must be followed by a comma.')
    symidx += 1

    # maybe invert it depending on "if so" vs "if not"
    fmt_str = 'if ({} {} {})' if if_type == SYM_IF_SO else 'if (!({} {} {}))'
    return symidx, speaker, spoken_to, fmt_str.format(expr1, op, expr2)
Exemplo n.º 13
0
def parse_expression(symbols, symidx, characters, speaker, spoken_to):
    """
    Parse an SPL expression into Java code.
    E.g. "sum of a large green cat and the difference between Romeo and a woman" -> "((2 * (2 * 1)) + (Romeo - 1))"
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :param characters: The list of characters.
    :param speaker: The character speaking.
    :param spoken_to: The character being spoken to.
    :returns: symidx, and the generated Java code representing that expression.
    :raises SplError: if there is an error.
    """

    if symidx >= len(symbols):
        raise SplError('Expression exceeded length of program.')

    if symbols[symidx][0] == SYM_TWICE or symbols[symidx][0] == SYM_ADJECTIVE:
        # twice, adjectives = (2*x)
        return parse_expr_prefix('(2*{})', symbols, symidx, characters,
                                 speaker, spoken_to)

    elif symbols[symidx][0] == SYM_THRICE:
        # thrice = (3*x)
        return parse_expr_prefix('(3*{})', symbols, symidx, characters,
                                 speaker, spoken_to)

    elif symbols[symidx][0] == SYM_SQUARE:
        # square = Math.pow(x, 2)
        return parse_expr_prefix('((int) Math.pow({}, 2))', symbols, symidx,
                                 characters, speaker, spoken_to)

    elif symbols[symidx][0] == SYM_CUBE:
        # cube = Math.pow(x, 3)
        return parse_expr_prefix('((int) Math.pow({}, 3))', symbols, symidx,
                                 characters, speaker, spoken_to)

    elif symbols[symidx][0] == SYM_SQUARE_ROOT:
        # square root = Math.sqrt(x)
        return parse_expr_prefix('((int) Math.sqrt({}))', symbols, symidx,
                                 characters, speaker, spoken_to)

    elif symbols[symidx][0] == SYM_CUBE_ROOT:
        # cube root = Math.cbrt(x)
        return parse_expr_prefix('((int) Math.cbrt({}))', symbols, symidx,
                                 characters, speaker, spoken_to)

    elif symbols[symidx][0] == SYM_HALF:
        # half = (x/2)
        return parse_expr_prefix('({}/2)', symbols, symidx, characters,
                                 speaker, spoken_to)

    elif symbols[symidx][0] == SYM_1ST_PERSON_PRONOUN:
        # first person pronouns = the speaker
        return symidx + 1, speaker

    elif symbols[symidx][0] == SYM_2ND_PERSON_PRONOUN:
        # second person pronouns = the person being spoken to
        return symidx + 1, spoken_to

    elif symbols[symidx][0] == SYM_CHARACTER:
        # characters = that character
        if symbols[symidx][1] not in characters:
            raise SplError(symbols[symidx][1] + ' is not in this program!')
        return symidx + 1, symbols[symidx][1]

    elif symbols[symidx][0] == SYM_POSITIVE_NOUN:
        # positive nouns = 1
        return symidx + 1, '1'

    elif symbols[symidx][0] == SYM_NEGATIVE_NOUN:
        # negative nouns = -1
        return symidx + 1, '-1'

    elif symbols[symidx][0] == SYM_ZERO:
        # zero: 0
        return symidx + 1, '0'

    elif symbols[symidx][0] == SYM_SUM:
        # sum: (x + y)
        return parse_expr_operator('({} + {})', symbols, symidx, characters,
                                   speaker, spoken_to)

    elif symbols[symidx][0] == SYM_DIFFERENCE:
        # difference: (x - y)
        return parse_expr_operator('({} - {})', symbols, symidx, characters,
                                   speaker, spoken_to)

    elif symbols[symidx][0] == SYM_PRODUCT:
        # product: (x * y)
        return parse_expr_operator('({} * {})', symbols, symidx, characters,
                                   speaker, spoken_to)

    elif symbols[symidx][0] == SYM_QUOTIENT:
        # quotient: (x / y)
        return parse_expr_operator('({} / {})', symbols, symidx, characters,
                                   speaker, spoken_to)

    elif symbols[symidx][0] == SYM_REMAINDER:
        # remainder of the quotient: (x % y)
        # there must be "quotient" first
        symidx += 1
        if symbols[symidx][0] != SYM_QUOTIENT:
            raise SplError('"Quotient" must appear after "remainder".')
        return parse_expr_operator('({} % {})', symbols, symidx, characters,
                                   speaker, spoken_to)

    elif symbols[symidx][0] == SYM_END_PUNCTUATION:
        # ended prematurely: give a more useful error
        raise SplError(
            'Expression ended too soon: did you use an unknown noun/adjective/etc?'
        )

    else:
        # unknown: error
        raise SplError('Unknown symbol in expression: ' + str(symbols[symidx]))
Exemplo n.º 14
0
def parse_stage_direction(symbols, symidx, characters, stage):
    """
    Parse a stage direction, making the necessary modifications to the stage.
    :param symbols: The list of symbols.
    :param symidx: The index into the list of symbols.
    :param characters: The list of characters in the program.
    :param stage: The set representing which characters are currently on stage.
    :returns: The stage and the symidx.
    :raises SplError: If there is an error.
    """

    # first open stage direction
    if symbols[symidx][0] != SYM_OPEN_STAGE_DIRECTION:
        raise SplError('Expected "[" to open a stage direction.')

    # then ENTER, EXIT, or EXEUNT
    symidx += 1
    sd_type = symbols[symidx][0]
    if sd_type not in (SYM_STAGE_DIRECTION_ENTER, SYM_STAGE_DIRECTION_EXIT,
                       SYM_STAGE_DIRECTION_EXEUNT):
        raise SplError(
            'Expected "Enter", "Exit", or "Exeunt" in stage direction.')

    symidx += 1

    if sd_type != SYM_STAGE_DIRECTION_EXEUNT:
        # parse the first character
        if symbols[symidx][0] != SYM_CHARACTER:
            raise SplError('Expected character after "Enter" or "Exit".')
        character1 = symbols[symidx][1]
        if character1 not in characters:
            raise SplError('Unknown character in stage direction: ' +
                           character1)
        symidx += 1

    if sd_type == SYM_STAGE_DIRECTION_ENTER:
        # enter can have an optional second character
        if symbols[symidx][0] == SYM_AND:
            symidx += 1
            if symbols[symidx][0] != SYM_CHARACTER:
                raise SplError(
                    'Expected second character after "and" in stage direction.'
                )
            character2 = symbols[symidx][1]
            if character2 not in characters:
                raise SplError('Unknown character in stage direction: ' +
                               character2)
            if character2 == character1:
                raise SplError(
                    "You can't enter the same character twice in the same stage direction."
                )
            symidx += 1
        else:
            character2 = None

    # now close stage direction
    if symbols[symidx][0] != SYM_CLOSE_STAGE_DIRECTION:
        raise SplError('Expected "]" to close a stage direction')
    symidx += 1

    # do the things to the stage
    if sd_type == SYM_STAGE_DIRECTION_ENTER:
        if len(stage) == 2 or (character2 is not None and len(stage) > 0):
            raise SplError('Too many characters on stage.')
        if character1 in stage:
            raise SplError(character1 + ' is already on stage.')

        stage.add(character1)
        if character2 is not None:
            stage.add(character2)
    elif sd_type == SYM_STAGE_DIRECTION_EXIT:
        if character1 not in stage:
            raise SplError(character1 + ' is not on stage and cannot exit.')
        stage.remove(character1)
    elif sd_type == SYM_STAGE_DIRECTION_EXEUNT:
        if len(stage) == 0:
            raise SplError('Stage is empty, cannot exeunt.')
        stage.clear()

    return stage, symidx