diff options
Diffstat (limited to 'tests/check.py')
| -rw-r--r-- | tests/check.py | 162 |
1 files changed, 140 insertions, 22 deletions
diff --git a/tests/check.py b/tests/check.py index 3100e64..df670ee 100644 --- a/tests/check.py +++ b/tests/check.py @@ -91,7 +91,7 @@ DIAGNOSTICS = [ # v0b: messages, keyed documents and types ('state d: bytes by key\ninput m = message\non m {\n reply d[key]\n}\n', '4:9', 'd[key] needs the handler of a message by key'), ('input m = message by key { a: u32 }\non m {\n reply m.a\n}\n', '3:9', 'reply takes bytes or text, not a number'), - ('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, number, bool, bytes, text and list of a record'), + ('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, i64, number, bool, bytes, text and list of a record'), ('input m = message by key\non m.down {\n}\n', '2:4', "'m' is a message; write on m { }"), ('input k = key Space\non k {\n}\n', '2:4', "'k' is a key input; write on k.down or on k.up"), ('state d: bytes by key\ninput m = message by key { a: bytes }\non m {\n d[1].replace(0 .. 0, m.a)\n}\n', '4:5', @@ -129,8 +129,8 @@ DIAGNOSTICS = [ '6:12', "a cursor of 'a' cannot point into 'b'"), ('state a: text\nstate at: cursor in a\ninput k = key KeyK\non k.down {\n at = 3\n}\n', '5:8', 'expected a cursor, not a number'), - ('state a: text\nstate at: cursor in a\nview v = at\n', '3:10', 'a view is a number, a bool or text, not a cursor'), - ('state a: text\nstate at: cursor in a\nview v = "{at}"\n', '3:12', 'a string shows numbers, bools and text, not a cursor'), + ('state a: text\nstate at: cursor in a\nview v = at\n', '3:10', 'a view is a number, a bool, an i64 or text, not a cursor'), + ('state a: text\nstate at: cursor in a\nview v = "{at}"\n', '3:12', 'a string shows numbers, bools, i64s and text, not a cursor'), ('state x = 1\nstate t: text = "{x}"\n', '2:17', 'a text state starts from a plain string'), ('view v = "a\\qb"\n', '1:12', 'unknown escape'), ('view v = "\\u{D800}"\n', '1:11', 'Unicode scalar'), @@ -145,7 +145,7 @@ DIAGNOSTICS = [ # records, lists and indices ('record T {\n a = 0\n a = 1\n}\n', '3:3', "duplicate field 'a'"), ('record T {\n}\n', '1:8', 'a record needs at least one field'), - ('record T {\n a: number\n}\n', '2:6', 'a field is a number (field = 0), a bool (field = false) or text (field: text)'), + ('record T {\n a: number\n}\n', '2:6', 'a field is a number (field = 0), a bool (field = false), text (field: text) or an i64 (field: i64)'), ('record T {\n a = 0\n}\nstate T = 0\n', '4:7', "'T' is already declared as a record"), ('state xs: list of Missing\n', '1:7', "a list holds a record; 'Missing' is not one"), ('state x = 0\nstate at: index of x\n', '2:7', "an index points into a list state; 'x' is not one"), @@ -156,7 +156,7 @@ DIAGNOSTICS = [ (RECORD + 'state xs: list of T\nstate at: index of xs\nview v = xs[at]\n', '6:10', 'read a field of the item'), (RECORD + 'state xs: list of T\nview v = xs\n', '5:10', "'xs' is a list; read xs.count"), (RECORD + 'state xs: list of T\nview v = xs.size\n', '5:10', "a list has count, first and last, not 'size'"), - (RECORD + 'state xs: list of T\nstate at: index of xs\nview v = at\n', '6:10', 'a view is a number, a bool or text, not an index'), + (RECORD + 'state xs: list of T\nstate at: index of xs\nview v = at\n', '6:10', 'a view is a number, a bool, an i64 or text, not an index'), (RECORD + 'state xs: list of T\nstate at: index of xs\n' + ON_K + ' at = 3\n}\n', '8:8', 'expected an index, not a number'), (RECORD + 'state xs: list of T\n' + ON_K + ' xs = 1\n}\n', '7:3', 'a list changes by append, insert, remove'), (RECORD + 'record U {\n a = 0\n}\nstate xs: list of T\n' + ON_K + ' xs.append(U { a: 1 })\n}\n', '10:13', @@ -191,7 +191,7 @@ DIAGNOSTICS = [ (LIST + 'view t = each x in xs {\n a = 1\n a = 2\n}\n', '8:3', "duplicate column 'a'"), (LIST + 'view t = each x in xs {\n a = x.a\n}\nview v = x.a\n', '9:10', "unknown state 'x'"), (LIST + 'view t = each at in xs {\n a = 1\n}\n', '6:15', "'at' is already declared as a list index"), - (LIST + 'view t = each x in xs {\n c = x\n}\n', '7:7', 'a column is a number, a bool or text, not an index'), + (LIST + 'view t = each x in xs {\n c = x\n}\n', '7:7', 'a column is a number, a bool, an i64 or text, not an index'), ('view each = 1\n', '1:6', "'each' is reserved"), # fns ('fn f(x) = x\nview v = f(1, 2)\n', '2:10', 'f expects 1 argument'), @@ -207,13 +207,13 @@ DIAGNOSTICS = [ (LIST + 'view v = count(x in xs where count(y in xs) > 0)\n', '6:30', 'a count, sum, max or min cannot hold another'), (LIST + 'view v = count(x in at)\n', '6:10', "'at' is not a list"), ('record T {\n t: text\n}\nstate xs: list of T\nview v = sum(x.t for x in xs)\n', '5:14', - 'sum, max, min and distinct take numbers, not text'), + 'sum, max, min and distinct take numbers or i64s, not text'), (LIST + 'fn f(n) = count(x in xs where x.a == n)\nview v = f(1)\n', '6:38', "'n' is a parameter outside this count, sum, max or min"), (LIST + 'view v = sum(dt for x in xs)\n', '6:14', 'dt is not available in count, sum, max or min'), (LIST + 'view v = sum(x.a for x xs)\n', '6:24', "expected 'in' and a list, as in sum(x in tasks)"), (LIST + 'view w = each r in xs where r {\n n = r.a\n}\n', '6:29', 'expected a bool, not an index'), - (LIST + 'view v = first(x in xs)\n', '6:10', 'a view is a number, a bool or text, not an index'), + (LIST + 'view v = first(x in xs)\n', '6:10', 'a view is a number, a bool, an i64 or text, not an index'), (LIST + 'view w = each r in xs where count(y in xs) > 0 {\n n = r.a\n}\n', '6:29', "a table's where cannot hold a count, sum, max or min"), # Keyed lists: a key field of the record, placed by add, found by key, never changed. @@ -276,7 +276,7 @@ DIAGNOSTICS = [ (ARGS + 'view v(k: u32, k: u32) = k\n', '6:16', "duplicate argument 'k'"), (ARGS + 'view v(n: u32) = n\n', '6:8', "'n' is already declared as a state"), (ARGS + 'view t(r: u32) = each r in xs {\n a = r.a\n}\n', '6:8', "'r' names the table's row"), - (ARGS + 'view v(k: list) = 1\n', '6:11', 'an argument is a u32, number, bool, bytes or text'), + (ARGS + 'view v(k: list) = 1\n', '6:11', 'an argument is a u32, i64, number, bool, bytes or text'), (ARGS + 'view v() = 1\n', '6:6', "'v()' takes no argument: write view v = ..."), (ARGS + 'view averylongviewnamethatistoolong(k: u32) = k\n', '6:6', 'a view with arguments has a name of at most 26 characters'), @@ -284,7 +284,7 @@ DIAGNOSTICS = [ # A let holds a number or an index for the rest of its block, in its handler. (LETS + 'on k.down {\n let n = 1\n}\n', '10:7', "'n' is already declared as a state"), (LETS + 'on k.down {\n let x = 1\n let x = 2\n}\n', '11:7', "'x' is already a let here"), - (LETS + 'on k.down {\n let x = t\n}\n', '10:11', 'a let holds a number, a bool or an index, not text'), + (LETS + 'on k.down {\n let x = t\n}\n', '10:11', 'a let holds a number, a bool, an i64 or an index, not text'), (LETS + 'on k.down {\n let x = 1\n x = xs.first\n}\n', '11:7', "'x' holds a number"), (LETS + 'on k.down {\n let i = xs.first\n i = ys.first\n}\n', '11:7', "'i' holds an index of its list"), (LETS + 'on k.down {\n if n > 0 {\n let x = 1\n }\n n = x\n}\n', '13:7', "unknown state 'x'"), @@ -292,8 +292,8 @@ DIAGNOSTICS = [ (LETS + 'view v = x\non k.down {\n let x = 1\n}\n', '9:10', "unknown state 'x'"), (LETS + 'on k.down {\n let let = 1\n}\n', '10:7', "'let' is reserved"), # A slice takes a list keyed by a number. - (LETS + 'view v = count(x in xs[0 .. 3])\n', '9:10', "'xs' is not keyed by a number: a slice takes one"), - (LETS + 'on k.down {\n for x in xs[0 .. 1] {\n n = 1\n }\n}\n', '10:12', "'xs' is not keyed by a number: a slice takes one"), + (LETS + 'view v = count(x in xs[0 .. 3])\n', '9:10', "'xs' is not keyed by a number or an i64: a slice takes one"), + (LETS + 'on k.down {\n for x in xs[0 .. 1] {\n n = 1\n }\n}\n', '10:12', "'xs' is not keyed by a number or an i64: a slice takes one"), # A bool is not a number: conditions take bools, and a number says how to become one. ('state n = 0\non frame {\n if n {\n n = 1\n }\n}\n', '3:6', 'if takes a bool, not a number: compare it, as in n != 0'), ('view v = if 1 { 2 } else { 3 }\n', '1:13', 'if takes a bool, not a number'), @@ -310,14 +310,50 @@ DIAGNOSTICS = [ ('state lit = true\nstate n = 0\non frame {\n n = lit\n}\n', '4:7', 'expected a number, not a bool'), ('state lit = true\non frame {\n lit = 1\n}\n', '3:9', 'expected a bool, not a number'), ('record T {\n a = false\n}\nstate xs: list of T by a\n', '4:24', "a list is keyed by a number or text; 'a' is a bool"), - ('record T {\n a = false\n}\nstate xs: list of T\nview v = sum(x.a for x in xs)\n', '5:', 'sum, max, min and distinct take numbers, not a bool'), + ('record T {\n a = false\n}\nstate xs: list of T\nview v = sum(x.a for x in xs)\n', '5:', 'sum, max, min and distinct take numbers or i64s, not a bool'), ('record T {\n a = false\n}\nstate xs: list of T\n' + ON_K + ' xs.append(T { a: 1 })\n}\n', '7:20', 'expected a bool, not a number'), ('input m = message {\n ok: bool\n}\nstate n = 0\non m {\n n = m.ok\n}\n', '6:7', 'expected a number, not a bool'), ('state lit = true\noutput o = message {\n n: number\n}\n' + ON_K + ' send o { n: lit }\n}\n', '7:15', 'expected a number, not a bool'), ('state true = 1\n', '1:7', "'true' is reserved"), + # An i64 is exact and whole, and does not mix with numbers: a literal takes the type the other side has. + ('state n: i64 = 1.5\n', '1:16', "expected a whole number for an i64 state"), + ('state n: i64 = 9223372036854775808\n', '1:16', "the number is past an i64's range"), + ('state n: i64\nstate x = 0\nview v = n + x\n', '3:14', 'expected an i64, not a number: convert it, as in i64(x)'), + ('state n: i64\nstate x = 0\nview v = x + n\n', '3:14', 'an i64 and a number do not mix: convert one, as in i64(x) or number(n)'), + ('state n: i64\non frame {\n n = 2.5\n}\n', '3:7', 'an i64 is a whole number; this one is not'), + ('state n: i64\nstate x = 0\non frame {\n n = x\n}\n', '4:7', 'expected an i64, not a number: convert it, as in i64(x)'), + ('state n: i64\nstate x = 0\non frame {\n x = n\n}\n', '4:7', 'expected a number, not an i64'), + ('state n: i64\nview v = clamp(n, 0, 1)\n', '2:16', 'expected a number, not an i64'), + ('state n: i64\nview v = floor(n)\n', '2:16', 'expected a number, not an i64'), + ('state n: i64\nview v = i64(n)\n', '2:14', 'expected a number, not an i64'), + ('state x = 0\nview v = number(x)\n', '2:17', 'expected an i64, not a number: convert it, as in i64(x)'), + ('record T {\n id: i64 = 1.5\n}\n', '2:13', "expected a whole number for an i64 field's default"), + ('record T {\n id: i64\n}\nstate xs: list of T by id\nview v = xs[1.5].id\n', '5:13', + "'xs' is keyed by id, an i64: expected an i64 or an index, not a number"), + ('state n: i64\nview v = if true { n } else { 1.5 }\n', '2:31', 'the branches of if give an i64 and a number'), + ('state n: i64\nview v = n < true\n', '2:14', 'expected an i64, not a bool'), + ('view v = 9223372036854775808 + i64(1)\n', '1:10', "the number is past an i64's range"), + ('input m = message {\n id: i64\n}\nstate x = 0\non m {\n x = m.id\n}\n', '6:7', 'expected a number, not an i64'), + ('record T {\n a: i64\n}\nstate xs: list of T\nview v = sum(x.a for x in xs) + 1.5\n', '5:33', + 'an i64 is a whole number; this one is not'), ] ACCEPTED = [ + # Literals beside numbers and i64s, nested deep: each chain is typed in one probe (a probe per level would take 2^30). + 'state x = 0\nstate y: i64\nview v = ' + '1 + (' * 30 + 'x' + ')' * 30 + '\nview w = ' + '1 + (' * 30 + 'y' + ')' * 30 + '\n', + # i64s: states, fields, keys, slices, messages, outputs, arguments, lets, arithmetic, conditionals, folds, text. + 'record Order {\n id: i64\n total: i64 = -1\n note: text\n}\nstate orders: list of Order by id\n' + 'state next: i64 = 9007199254740993\nstate sum: i64\nstate seen = 0\ninput put = message {\n id: i64\n total: i64\n}\n' + 'output told = message {\n id: i64\n big: bool\n}\ninput go = key KeyG\n' + 'on put {\n orders.add(Order { id: put.id, total: put.total * 2 + 1, note: "{put.id}" })\n' + ' send told { id: put.id, big: put.total > 1000 }\n}\n' + 'on go.down {\n let step: = 0\n}\n'.replace(' let step: = 0\n', ' let step = next / 3 % 7 - 1\n next = next + step\n' + ' sum = sum + max(-step, 0) + min(1, next)\n seen = number(step) + seen\n' + ' for o in orders[0 .. next] where o.total != 0 {\n o.total = -o.total\n }\n if orders.has(5) { orders[5].total = 9 }\n') + + 'view count = count(o in orders[-5 .. 5])\nview most = max(o.total for o in orders)\nview kinds = distinct(o.total for o in orders)\n' + 'view pick = if seen > 0 { next } else if seen < 0 { 0 } else { -1 }\nview line = "{next}/{sum}"\n' + 'view from(low: i64) = sum(o.total for o in orders where o.id >= low)\nview back = i64(2.0) + i64(seen)\n' + 'view board = each o in orders where o.id > 0 {\n id = o.id\n total = o.total\n}\n', # Bools: states, fields, message and output fields, arguments, lets, views, columns, interpolation, has. 'state lit = false\nstate n = 0\nrecord T {\n done = false\n title: text\n}\nstate ts: list of T\n' 'record K {\n name: text\n flag = true\n}\nstate ks: list of K by name\ninput flip = key Space\n' @@ -508,9 +544,11 @@ KEYS = ['Space', 'Enter', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyQ', 'Digit0', ' def random_program(rng): - """A random well-formed program of numbers and bools: every one must compile and verify.""" - states = [(f's{i}', rng.random() < 0.3) for i in range(rng.randint(1, 6))] - numbers, flags = [name for name, flag in states if not flag], [name for name, flag in states if flag] + """A random well-formed program of numbers, bools and i64s: every one must compile and verify.""" + states = [(f's{i}', rng.choice(['number', 'number', 'bool', 'i64'])) for i in range(rng.randint(1, 6))] + numbers = [name for name, kind in states if kind == 'number'] + flags = [name for name, kind in states if kind == 'bool'] + integers = [name for name, kind in states if kind == 'i64'] inputs = [f'i{i}' for i in range(rng.randint(0, 4))] keys = rng.sample(KEYS, len(inputs)) @@ -520,26 +558,50 @@ def random_program(rng): return rng.choice([str(rng.choice([0, 1, 2, 0.5, 1e-3, 1e300, 7.25]))] + numbers + (['dt'] if handler else [])) if choice < 0.35: return '-' + number(depth - 1, handler) - if choice < 0.5: + if choice < 0.45: name, count = rng.choice([('min', 2), ('max', 2), ('clamp', 3)]) return f'{name}(' + ', '.join(number(depth - 1, handler) for _ in range(count)) + ')' + if choice < 0.5: + return f'number({integer(depth - 1, handler)})' if choice < 0.6: return f'(if {truth(depth - 1, handler)} {{ {number(depth - 1, handler)} }} else {{ {number(depth - 1, handler)} }})' op = rng.choice(['+', '-', '*', '/']) return f'({number(depth - 1, handler)} {op} {number(depth - 1, handler)})' + def integer(depth, handler): + choice = rng.random() + if depth <= 0 or choice < 0.25: + literal = str(rng.choice([0, 1, 7, -3, 2**31, 2**53 + 1, 2**62, -2**62, 2**63 - 1])) + return rng.choice([f'i64({literal})' if not integers or rng.random() < 0.5 else literal] + integers) + if choice < 0.35: + return f'i64({number(depth - 1, handler)})' + if choice < 0.45: + return f'{rng.choice(["min", "max"])}({integer(depth - 1, handler)}, {integer(depth - 1, handler)})' + if choice < 0.55: + return f'(if {truth(depth - 1, handler)} {{ {integer(depth - 1, handler)} }} else {{ {rng.choice(["0", "-1"])} }})' + if choice < 0.6: + return '-' + integer(depth - 1, handler) + op = rng.choice(['+', '-', '*', '/', '%']) + right = str(rng.choice([1, 2, 10, -3])) if rng.random() < 0.3 else integer(depth - 1, handler) + return f'({integer(depth - 1, handler)} {op} {right})' + def truth(depth, handler): choice = rng.random() if depth <= 0 or choice < 0.2: return rng.choice(['true', 'false'] + flags) if choice < 0.35: return '!' + truth(depth - 1, handler) - if choice < 0.7: + if choice < 0.55: op = rng.choice(['<', '<=', '>', '>=', '==', '!=']) return f'({number(depth - 1, handler)} {op} {number(depth - 1, handler)})' + if choice < 0.7: + op = rng.choice(['<', '<=', '>', '>=', '==', '!=']) + return f'({integer(depth - 1, handler)} {op} {integer(depth - 1, handler)})' op = rng.choice(['&&', '||', '==', '!=']) return f'({truth(depth - 1, handler)} {op} {truth(depth - 1, handler)})' + make = {'number': number, 'bool': truth, 'i64': integer} + def body(depth, indent): lines = [] for _ in range(rng.randint(0, 3)): @@ -551,13 +613,14 @@ def random_program(rng): lines += body(depth - 1, indent + ' ') lines.append(f'{indent}}}') else: - name, flag = rng.choice(states) - lines.append(f'{indent}{name} = {truth(3, True) if flag else number(3, True)}') + name, kind = rng.choice(states) + lines.append(f'{indent}{name} = {make[kind](3, True)}') return lines - lines = [f'state {name} = {rng.choice(["true", "false"] if flag else ["0", "-1", "3.5", "+2"])}' for name, flag in states] + starts = {'number': ['0', '-1', '3.5', '+2'], 'bool': ['true', 'false'], 'i64': ['0', '-9', '9007199254740993']} + lines = [f'state {name}{": i64" if kind == "i64" else ""} = {rng.choice(starts[kind])}' for name, kind in states] lines += [f'input {name} = key {key}' for name, key in zip(inputs, keys)] - lines += [f'view v{i} = {truth(3, False) if rng.random() < 0.3 else number(3, False)}' for i in range(rng.randint(0, 3))] + lines += [f'view v{i} = {rng.choice(list(make.values()))(3, False)}' for i in range(rng.randint(0, 3))] events = ['frame'] + [f'{name}.{edge}' for name in inputs for edge in ('down', 'up')] for _ in range(rng.randint(0, 5)): lines.append(f'on {rng.choice(events)} {{') @@ -1348,6 +1411,60 @@ def bools(aup, pure_run, work): return len(shown) +I64_SOURCE = """record Row { + id: i64 + n = 0 +} +state rows: list of Row by id +state next: i64 = 9007199254740990 +state n = 0 +input add = key KeyA +input grow = key KeyG +input half = key KeyH +input far = key KeyF +on add.down { + rows.add(Row { id: next, n: n }) + next = next + 1 + n = n + 1 +} +on grow.down { next = next * 1024 } +on half.down { next = -next / 2 } +on far.down { n = number(next) } +view total = sum(r.id for r in rows) +view line = "{next} {i64(-7) / 2} {i64(-7) % 2} {i64(7) % -2} {-7 / 2}" +view least = min(r.id for r in rows) +""" + + +def integers(aup, pure_run, work): + """i64s are exact past 2^53 in every output (states, items, views, text), + arithmetic past their range fails as overflow and changes nothing, + division and remainders floor, a conversion to a number fails where + the value does not fit, and a module replaced keeps what fits.""" + (work / 'integers.aup').write_text(I64_SOURCE) + (work / 'integers2.aup').write_text('record Row {\n id = 0\n n: i64\n}\nstate rows: list of Row\nstate next = 0\nstate n: i64\n') + compile_module(aup, work / 'integers.aup', work / 'integers.pmod') + compile_module(aup, work / 'integers2.aup', work / 'integers2.pmod') + commands = ['show', 'down add', 'down add', 'down add', 'down add', 'down far', 'down grow', 'down half', + f'replace {work / "integers2.pmod"}'] + shown = [json.loads(line) for line in run(pure_run, work / 'integers.pmod', stdin='\n'.join(commands) + '\n', + check=False).stdout.splitlines()] + start = 2**53 - 2 # four rows: the last past 2^53 + rows = [{'id': start + k, 'n': k} for k in range(4)] + assert strict(shown[0]['states'], {'rows': [], 'next': start, 'n': 0}), shown[0] + assert shown[0]['views']['line'] == f'{start} -4 1 -1 -3.5', shown[0] # i64s floored; literals alone are numbers + assert strict(shown[4]['states'], {'rows': rows, 'next': start + 4, 'n': 4}), shown[4] + assert strict(shown[4]['views'], {'total': 4 * start + 6, 'line': f'{start + 4} -4 1 -1 -3.5', 'least': start}), shown[4] + assert shown[5]['fault'] == 'limit' and shown[5]['states']['n'] == 4, shown[5] # 2^53 + 2 is no number + assert shown[6]['fault'] == 'overflow' and shown[6]['states']['next'] == start + 4, shown[6] + assert shown[7]['states']['next'] == -(start + 4) // 2, shown[7] # floored, as Python's // + # Replaced: next, within 2^53 now, a number; n an i64; ids past 2^53 no number (the default), the rest kept. + final = shown[8] + assert strict(final['states'], {'rows': [{'id': start + k if k < 3 else 0, 'n': k} for k in range(4)], + 'next': -(start + 4) // 2, 'n': 4}), final + return len(shown) + + def text_parity(aup, pure_run, work, seeds, steps=400): module = work / 'notes.pmod' compile_module(aup, ROOT / 'examples/notes.aup', module) @@ -1500,6 +1617,7 @@ def main(): tasks_parity(aup, pure_run, work, range(args.text_seeds)) report['conditional_snapshots'] = conditionals(aup, pure_run, work) report['bool_snapshots'] = bools(aup, pure_run, work) + report['i64_snapshots'] = integers(aup, pure_run, work) report['time_snapshots'] = timing(aup, pure_run, work, range(args.seeds)) report['watched_builds'] = watching(aup, pure_run, work) report['fold_snapshots'] = folds(aup, pure_run, work, range(args.seeds if hasattr(args, 'seeds') else 8)) |
