import re

def varrepl(matched_text):
    num = int(matched_text[1:])
    if num > 0:
        num -= 1
    return f'variables[{num}]'

VAR_PATTERN = re.compile(r'\$-?\d+')
VAR_PATTERN_END = re.compile(r'\$-?\d+$')

def compile_python_expressions(input_string, validator: lambda expr: True):
    expressions = []
    remaining_text = input_string
    while remaining_text:
        expr, expr_text, remaining_text = greedy_parse(remaining_text, validator)
        expressions.append(expr_text)
    return expressions

def greedy_parse(s, validator)
    assert s
    first_error = None
    expr_text = None
    expr_matches = None
    remaining_text = None
    seen_string = ''
    for char in s:
        seen_string += char
        try_parse_string = re.sub(VAR_PATTERN, 'variables[0]', seen_string)
        try:
            expr = validator(try_parse_string)
            expr = ast.parse(try_parse_string, mode='eval')
            expr_matches = re.finditer(VAR_PATTERN, seen_string) 
            expr_text = seen_string
            remaining_text = s[len(seen_string):]
        except SyntaxError as e:
            first_error = first_error or e
    if expr is None:
        raise first_error
    replaced_text = replace_matches(expr_matches, expr_text)
    return validator(replaced_text), replaced_text, remaining_text

def replace_matches(matches, expr_text):
    '''
    find which $int matches we need to replace (Names) and which we
    don't (inside strings). Then replace whichever matches raised
    syntax errors
    '''
    replace_matches = []
    # replace in reversed order to preserve positions of earlier matches
    matches = list(reversed(list(matches)))
    for match in matches:
        teststr = expr_text
        test_matches = (m for m in matches if m is not match)
        for m in test_matches:
            teststr = teststr[:m.start()] + 'variables[0]' + teststr[m.end():]
        try:
            ast.parse(teststr, mode='eval')
        except SyntaxError:
            replace_matches.append(match)
    # replace the matches that raised syntax error if not changed
    replaced_text = expr_text
    for m in replace_matches:
        old = expr_text[m.start():m.end()]
        assert( VAR_PATTERN_END.match(old))
        replaced_text = replaced_text[:m.start()] + varrepl(old) + replaced_text[m.end():]
    return replaced_text

def on_error(s, offset):
    remainder = s[offset:]
    for pattern, handler in error_map.items():
        match = pattern.match(remainder)
        if match is not None:
            return handler(s, offset, match)

vals = [
    '"hello world"',
    '"hello" 42',
    '$1',
    '$1 0 or 7',
]

# for val in vals:
#     comp = compile_python_expressions(val)
#     for exp in comp:
#         print(eval(exp[0]))
    
@@ -376,14 +376,8 @@
     
- expr,
  exp
@@ -437,24 +437,24 @@
  validator)%0A
+
         expr
@@ -802,55 +802,8 @@
 ry:%0A
-            expr = validator(try_parse_string)%0A
     
@@ -850,32 +850,218 @@
 g, mode='eval')%0A
+        except SyntaxError as e:%0A            first_error = first_error or e%0A        else:%0A            if not validator(expr):%0A                remaining_text = None%0A                break%0A
             expr
@@ -1200,84 +1200,8 @@
 ):%5D%0A
-        except SyntaxError as e:%0A            first_error = first_error or e%0A
     
@@ -1318,34 +1318,8 @@
 turn
- validator(replaced_text),
  rep
@@ -2350,24 +2350,24 @@
 t%5Bm.end():%5D%0A
+
     return r
@@ -2382,420 +2382,8 @@
 text
-%0A%0Adef on_error(s, offset):%0A    remainder = s%5Boffset:%5D%0A    for pattern, handler in error_map.items():%0A        match = pattern.match(remainder)%0A        if match is not None:%0A            return handler(s, offset, match)%0A%0Avals = %5B%0A    '%22hello world%22',%0A    '%22hello%22 42',%0A    '$1',%0A    '$1 0 or 7',%0A%5D%0A%0A# for val in vals:%0A#     comp = compile_python_expressions(val)%0A#     for exp in comp:%0A#         print(eval(exp%5B0%5D))
 %0A   

Update pyexpr.py