├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── arraymodel.py ├── calc.py ├── doperators.py ├── functions.py ├── moperators.py ├── rgspl.py └── tests ├── README.md ├── test_data.py ├── test_functions.py ├── test_tokenizer.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | */__pycache__/* 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "math" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rodrigo Girão Serrão 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RGSPL 2 | 3 | RGSPL stands for RGS's Programming Language, a play on the fact that this project is an interpreter for the APL programming language (APL literally stands for A Programming Language). 4 | 5 | You can also read about this [in my blog](https://mathspp.com/blog/tag:lsbasi-apl#body-wrapper). 6 | -------------------------------------------------------------------------------- /arraymodel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to define the array model for APL. 3 | """ 4 | 5 | import math 6 | 7 | class APLArray: 8 | """Class to hold APL arrays. 9 | 10 | All arrays have a shape of type list and a list with the data. 11 | The length of the data attribute is always the product of all 12 | elements in the shape list, even for scalars (the product 13 | of an empty list is 1). 14 | """ 15 | 16 | def __init__(self, shape, data): 17 | self.shape = shape 18 | self.data = data 19 | 20 | def major_cells(self): 21 | """Returns an APLArray with the major cells of self.""" 22 | return self.n_cells(len(self.shape)-1) 23 | 24 | def n_cells(self, n): 25 | """Returns an APLArray with the n-cells of self. 26 | 27 | An array of rank r has r-cells, (r-1)-cells, ..., 0-cells. 28 | An n-cell is a subarray with n trailing dimensions. 29 | 30 | The result of asking for the n-cells of an array of rank r 31 | is an APLArray of rank (r-n) with shape equal to the first 32 | (r-n) elements of the shape of the original array. 33 | """ 34 | 35 | if n == 0: 36 | return self 37 | 38 | r = len(self.shape) 39 | if n > r: 40 | raise ValueError(f"Array of rank {r} does not have {n}-cells.") 41 | 42 | if r == n: 43 | return S(self) 44 | 45 | result_shape = self.shape[:r-n] 46 | cell_shape = self.shape[r-n:] 47 | size = math.prod(cell_shape) 48 | data = [ 49 | APLArray(cell_shape, self.data[i*size:(i+1)*size]) 50 | for i in range(math.prod(result_shape)) 51 | ] 52 | return APLArray(result_shape, data) 53 | 54 | def is_scalar(self): 55 | return not self.shape 56 | 57 | def is_simple_scalar(self): 58 | return (not self.shape) and (not isinstance(self.data[0], APLArray)) 59 | 60 | def at_least_vector(self): 61 | if self.is_simple_scalar(): 62 | return APLArray([1], [self]) 63 | else: 64 | return self 65 | 66 | def __str__(self): 67 | # Print simple scalars nicely. 68 | if self.is_simple_scalar(): 69 | return _simple_scalar_str(self.data[0]) 70 | 71 | # Print simple arrays next. 72 | if self.shape and all(d.is_simple_scalar() for d in self.data): 73 | strs = list(map(str, self.data)) 74 | rank = len(self.shape) 75 | if rank > 1: 76 | widths = [0 for _ in range(self.shape[-1])] 77 | for i, s in enumerate(strs): 78 | idx = i%self.shape[-1] 79 | widths[idx] = max(widths[idx], len(s)) 80 | for i, s in enumerate(strs): 81 | # Pad left with required number of spaces. 82 | strs[i] = (widths[i%self.shape[-1]]-len(s))*" " + s 83 | # Pad everything in array of rank 2 or more. 84 | # We add as many newlines as dimensions we just completed. 85 | cumulative_dim_sizes = [math.prod(self.shape[-i-1:]) for i in range(rank-1)] 86 | string = " " 87 | for i, s in enumerate(strs): 88 | string += sum(i!=0 and 0 == i%l for l in cumulative_dim_sizes)*"\n " 89 | string += s + " " 90 | return string 91 | 92 | # Print nested arrays next. 93 | # Scalars will print like 1-item vectors and vectors print like 1-row matrices. 94 | # Higher-rank arrays print like matrices spaced out vertically. 95 | # Start by finding the str representation of each element of the array and then 96 | # lay everything out, framing with nice characters like └┴┘├┼┤┌┬┐─│. 97 | rank = len(self.shape) 98 | if not rank: 99 | return str(APLArray([1, 1], self.data)) 100 | elif rank == 1: 101 | return str(APLArray([1]+self.shape, self.data)) 102 | else: 103 | # Find how many rows and columns each element needs. 104 | strs = [] 105 | trailing_size = self.shape[-1] 106 | widths = [0 for _ in range(trailing_size)] 107 | height = 0 108 | for i, d in enumerate(self.data): 109 | # If d is a non-simple scalar, print the data instead of the scalar. 110 | s = str(d) 111 | s_height = 1+s.count("\n") 112 | height = max(height, s_height) 113 | s_width = max(map(len, s.split("\n"))) 114 | widths[i%trailing_size] = max(widths[i%trailing_size], s_width) 115 | strs.append(s) 116 | 117 | # Build the sub-matrices with these dimensions. 118 | matrix_size = self.shape[-2]*self.shape[-1] 119 | n_matrices = math.prod(self.shape)//matrix_size 120 | matrices = [] 121 | for i in range(n_matrices): 122 | m = _frame_matrix(strs[i*matrix_size:(i+1)*matrix_size], widths, height) 123 | matrices.append(m) 124 | 125 | # Concatenate all the matrices and leave the appropriate newlines in between. 126 | newline_offsets = [math.prod(self.shape[-i-1:-2]) for i in range(rank-2)] 127 | string = "" 128 | for i, m in enumerate(matrices): 129 | string += sum(i!=0 and 0 == i%l for l in newline_offsets)*"\n" 130 | string += m + " " 131 | return string[:-1] 132 | 133 | def __repr__(self): 134 | """Unambiguous representation of an APLArray instance.""" 135 | return f"APLArray({repr(self.shape)}, {repr(self.data)})" 136 | 137 | def __eq__(self, other): 138 | return ( 139 | isinstance(other, APLArray) and 140 | self.shape == other.shape and 141 | self.data == other.data 142 | ) 143 | 144 | # Helper method to create APLArray scalars. 145 | S = lambda v: APLArray([], [v]) 146 | 147 | def _simple_scalar_str(s): 148 | """String representation of a simple scalar.""" 149 | 150 | if isinstance(s, complex): 151 | return "J".join(map(_simple_scalar_str, [s.real, s.imag])) 152 | # Non-complex numeric type: 153 | elif s < 0: 154 | return f"¯{-s}" 155 | elif int(s) == s: 156 | return str(int(s)) 157 | else: 158 | return str(s) 159 | 160 | def _frame_matrix(strs, widths, height): 161 | """Frames the values of a matrix with └┴┘├┼┤┌┬┐─│. 162 | 163 | `height` gives the vertical space each matrix element should occupy. 164 | `widths` gives the horizontal space each column in the matrix should occupy. 165 | The matrix has as many columns as `widths` has elements and the number 166 | of rows is `len(strs)/len(widths)`. 167 | """ 168 | 169 | ncols = len(widths) 170 | nrows = len(strs)//ncols 171 | boxes = [] 172 | for i, s in enumerate(strs): 173 | boxes.append(_box(s, widths[i%ncols], height)) 174 | 175 | rows = [] 176 | for i in range(nrows): 177 | row = _block_join("│", boxes[i*len(widths):(i+1)*len(widths)]) 178 | row = _block_prepend(row, "│") 179 | row = _block_append(row, "│") 180 | rows.append(row) 181 | 182 | # Get a matrix reference line to build the top, intermediate and bottom lines. 183 | ref_line = rows[0].split("\n")[0] 184 | top = "┌" 185 | for char in ref_line[1:-1]: 186 | top += "┬" if char == "│" else "─" 187 | top += "┐" 188 | intermediate = "├" + top[1:-1].replace("┬", "┼") + "┤" 189 | bot = "└" + top[1:-1].replace("┬", "┴") + "┘" 190 | 191 | # Put everything together. 192 | lines = [top] 193 | for row in rows: 194 | lines.extend(row.split("\n")) 195 | lines.append(intermediate) 196 | lines[-1] = bot 197 | return "\n".join(lines) 198 | 199 | def _box(s, w, h): 200 | """Make s have h lines, each of length w, with s on the top-left corner.""" 201 | 202 | lines = s.split("\n") 203 | lines = list(map(lambda l: l + (w-len(l))*" ", lines)) 204 | lines = lines + [" "*w]*(h-len(lines)) 205 | return "\n".join(lines) 206 | 207 | def _block_join(sep, blocks): 208 | """Join a sequence of appropriately sized blocks with the given separator.""" 209 | 210 | if not blocks: 211 | return "" 212 | r = blocks[0] 213 | for b in blocks[1:]: 214 | r = _block_concat(_block_append(r, sep), b) 215 | return r 216 | 217 | def _block_concat(left, right): 218 | """Concatenate two blocks of lines of appropriate shapes.""" 219 | 220 | return "\n".join( 221 | l+r for l, r in zip(left.split("\n"), right.split("\n")) 222 | ) 223 | 224 | def _block_prepend(string, val): 225 | """Prepend val to each line of string.""" 226 | return "\n".join( 227 | map(lambda l: val+l, string.split("\n")) 228 | ) 229 | 230 | def _block_append(string, val): 231 | """Append val to each line of string.""" 232 | return "\n".join( 233 | map(lambda l: l+val, string.split("\n")) 234 | ) 235 | -------------------------------------------------------------------------------- /calc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implement a basic calculator with APL-like syntax. 3 | 4 | This simple calculator only implements the operators +-×÷ and ¯ on simple integer scalars. 5 | This is the grammar accepted, where rules match from right to left. 6 | STATEMENT := EOL (TERM OP)* TERM 7 | TERM := NUM | "(" STATEMENT ")" 8 | NUM := "¯"? INTEGER 9 | OP := "+" | "-" | "×" | "÷" 10 | """ 11 | 12 | import abc 13 | 14 | # Token types 15 | # EOL (end-of-line) token is used to indicate that there is no more input left. 16 | INTEGER, NEGATE, EOL = "INTEGER", "NEGATE", "EOL" 17 | LPARENS, RPARENS = "LPARENS", "RPARENS" 18 | PLUS, MINUS, TIMES, DIVISION = "PLUS", "MINUS", "TIMES", "DIVISION" 19 | OPS = { 20 | PLUS: lambda l, r: l+r, 21 | MINUS: lambda l, r: l-r, 22 | TIMES: lambda l, r: l*r, 23 | DIVISION: lambda l, r: l/r, 24 | } 25 | 26 | def error(msg="Error with RGSPL."): 27 | """Raise an exception from the RGSPL inner workings.""" 28 | raise Exception(msg) 29 | 30 | class Token: 31 | """Token class for our interpreter.""" 32 | 33 | def __init__(self, type_, value): 34 | # Token type, e.g. INTEGER or LPARENS 35 | self.type = type_ 36 | # Token value, e.g. 45 or "(" 37 | self.value = value 38 | 39 | def __str__(self): 40 | """String representation of a Token instance. 41 | 42 | Examples: 43 | Token(INTEGER, 3) 44 | Token(PLUS, "+") 45 | """ 46 | return f"Token({self.type}, {self.value})" 47 | 48 | def __repr__(self): 49 | return self.__str__() 50 | 51 | 52 | class Lexer: 53 | """A lexer instance has the job of breaking down code into tokens.""" 54 | 55 | OP_CHARS = "+-÷ׯ" 56 | 57 | def __init__(self, text): 58 | self.text = text 59 | # We traverse from right to left 60 | self.pos = len(self.text) - 1 61 | self.current_char = self.text[self.pos] 62 | self.current_token = None 63 | 64 | def advance(self): 65 | """Advance internal pointer and get the next character.""" 66 | 67 | self.pos -= 1 68 | if self.pos < 0: 69 | self.current_char = None 70 | else: 71 | self.current_char = self.text[self.pos] 72 | 73 | def skip_whitespace(self): 74 | """Ignores whitespace.""" 75 | 76 | while self.current_char and self.current_char.isspace(): 77 | self.advance() 78 | 79 | def get_integer_token(self): 80 | """Creates a multidigit integer token.""" 81 | 82 | stop = self.pos + 1 83 | while self.current_char and self.current_char.isdigit(): 84 | self.advance() 85 | num = self.text[self.pos + 1:stop] 86 | return Token(INTEGER, int(num)) 87 | 88 | def get_op_token(self): 89 | """Returns an operator token.""" 90 | 91 | tok = None 92 | if self.current_char == "+": 93 | tok = Token(PLUS, "+") 94 | elif self.current_char == "-": 95 | tok = Token(MINUS, "-") 96 | elif self.current_char == "×": 97 | tok = Token(TIMES, "×") 98 | elif self.current_char == "÷": 99 | tok = Token(DIVISION, "÷") 100 | elif self.current_char == "¯": 101 | tok = Token(NEGATE, "¯") 102 | 103 | if tok is not None: 104 | self.advance() 105 | return tok 106 | else: 107 | return error("Could not parse operator token.") 108 | 109 | def get_next_token(self): 110 | """Lexical analyzer (aka scanner or tokenizer) 111 | 112 | This method is responsible for breaking a sentence into tokens, one at a time. 113 | """ 114 | 115 | self.skip_whitespace() 116 | # Check if we already parsed everything. 117 | if not self.current_char: 118 | return Token(EOL, None) 119 | 120 | # Check what type of token we have now. 121 | if self.current_char.isdigit(): 122 | return self.get_integer_token() 123 | 124 | if self.current_char in self.OP_CHARS: 125 | return self.get_op_token() 126 | 127 | if self.current_char == ")": 128 | self.advance() 129 | return Token(RPARENS, ")") 130 | elif self.current_char == "(": 131 | self.advance() 132 | return Token(LPARENS, "(") 133 | 134 | error("Could not parse an appropriate token.") 135 | 136 | class ASTNode(abc.ABC): 137 | """Base class for all the ASTNode classes.""" 138 | 139 | class UnFunc(ASTNode): 140 | """Node for unary operations.""" 141 | def __init__(self, op, child): 142 | self.token = self.op = op 143 | self.child = child 144 | 145 | def __str__(self): 146 | return f"{self.token} [{self.child}]" 147 | 148 | class BinFunc(ASTNode): 149 | """Node for binary operations.""" 150 | def __init__(self, op, left, right): 151 | self.token = self.op = op 152 | self.left = left 153 | self.right = right 154 | 155 | def __str__(self): 156 | return f"{self.token} [{self.left} {self.right}]" 157 | 158 | class Num(ASTNode): 159 | """Node for numbers.""" 160 | def __init__(self, token): 161 | self.token = token 162 | self.value = self.token.value 163 | 164 | def __str__(self): 165 | return f"{self.token}" 166 | 167 | class Parser: 168 | """Parses code into an Abstract Syntax Tree (AST).""" 169 | 170 | def __init__(self, lexer): 171 | # Client string input, e.g. "6×3+5" 172 | self.lexer = lexer 173 | # Current token instance 174 | self.current_token = self.lexer.get_next_token() 175 | 176 | def eat(self, token_type): 177 | """Compare the current token with the expected token type.""" 178 | 179 | if self.current_token.type == token_type: 180 | self.current_token = self.lexer.get_next_token() 181 | else: 182 | error(f"Expected type {token_type} and got {self.current_token}") 183 | 184 | def num(self): 185 | """Parses a NUM.""" 186 | 187 | node = Num(self.current_token) 188 | self.eat(INTEGER) 189 | 190 | if self.current_token.type == NEGATE: 191 | node = UnFunc(self.current_token, node) 192 | self.eat(NEGATE) 193 | 194 | return node 195 | 196 | def term(self): 197 | """Parses a TERM.""" 198 | 199 | if self.current_token.type == RPARENS: 200 | self.eat(RPARENS) 201 | node = self.statement() 202 | self.eat(LPARENS) 203 | else: 204 | node = self.num() 205 | 206 | return node 207 | 208 | def statement(self): 209 | """Parses a STATEMENT.""" 210 | 211 | node = self.term() 212 | while self.current_token.type in [PLUS, MINUS, TIMES, DIVISION]: 213 | op = self.current_token 214 | self.eat(self.current_token.type) 215 | left_node = self.term() 216 | 217 | node = BinFunc(op, left_node, node) 218 | 219 | return node 220 | 221 | def parse(self): 222 | """Parses the client string.""" 223 | 224 | node = self.statement() 225 | self.eat(EOL) 226 | return node 227 | 228 | class NodeVisitor: 229 | """Any type of interpreter must inherit from this base class.""" 230 | def visit(self, node): 231 | """Dispatch the correct visit method for a given node.""" 232 | method_name = f"visit_{type(node).__name__}" 233 | visitor = getattr(self, method_name, self.generic_visit) 234 | return visitor(node) 235 | 236 | def generic_visit(self, node): 237 | """Triggers an error for visitors that haven't been implemented.""" 238 | error(f"No visitor for {type(node).__name__}!") 239 | 240 | class Interpreter(NodeVisitor): 241 | """Interprets the code with the NodeVisitor pattern.""" 242 | 243 | def __init__(self, parser): 244 | self.parser = parser 245 | 246 | def visit_Num(self, node): 247 | """Visit a Num node and return its intrinsic value.""" 248 | return node.value 249 | 250 | def visit_UnFunc(self, node): 251 | """Visit a UnFunc and apply its operation to its child.""" 252 | 253 | value = self.visit(node.child) 254 | if node.token.type == NEGATE: 255 | return -value 256 | error(f"Could not visit UnFunc {node}") 257 | 258 | def visit_BinFunc(self, node): 259 | """Visit a BinFunc and apply its operation to its children.""" 260 | 261 | right = self.visit(node.right) 262 | left = self.visit(node.left) 263 | func = OPS.get(node.token.type, None) 264 | if func: 265 | return func(left, right) 266 | error(f"Could not visit BinFunc {node}") 267 | 268 | def interpret(self): 269 | """Interprets a given piece of code and returns its result.""" 270 | tree = self.parser.parse() 271 | return self.visit(tree) 272 | 273 | 274 | def main(): 275 | """Run a REPL to test the code.""" 276 | 277 | while inp := input(" >> "): 278 | lexer = Lexer(inp) 279 | parser = Parser(lexer) 280 | interpreter = Interpreter(parser) 281 | result = interpreter.interpret() 282 | print(result) 283 | 284 | 285 | if __name__ == "__main__": 286 | main() 287 | -------------------------------------------------------------------------------- /doperators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module that implements APL's dyadic operators. 3 | """ 4 | 5 | def jot(*, aalpha, oomega): 6 | """Define the dyadic jot ∘ operator. 7 | 8 | Monadic case: 9 | f∘g ⍵ 10 | f g ⍵ 11 | Dyadic case: 12 | ⍺ f∘g ⍵ 13 | ⍺ f g ⍵ 14 | """ 15 | 16 | def derived(*, alpha=None, omega): 17 | return aalpha(alpha=alpha, omega=oomega(omega=omega)) 18 | return derived 19 | 20 | def atop(*, aalpha, oomega): 21 | """Define the dyadic atop ⍤ operator. 22 | 23 | Monadic case: 24 | f⍤g ⍵ 25 | f g ⍵ 26 | Dyadic case: 27 | ⍺ f⍤g ⍵ 28 | f ⍺ g ⍵ 29 | """ 30 | 31 | def derived(*, alpha=None, omega): 32 | return aalpha(alpha=None, omega=oomega(alpha=alpha, omega=omega)) 33 | return derived 34 | 35 | def over(*, aalpha, oomega): 36 | """Define the dyadic over ⍥ operator. 37 | 38 | Monadic case: 39 | f⍥g ⍵ 40 | f g ⍵ 41 | Dyadic case: 42 | ⍺ f⍥g ⍵ 43 | (g ⍺) f (g ⍵) 44 | """ 45 | 46 | def derived(*, alpha=None, omega): 47 | if alpha is None: 48 | return aalpha(alpha=alpha, omega=oomega(omega=omega)) 49 | else: 50 | return aalpha(alpha=oomega(omega=alpha), omega=oomega(omega=omega)) 51 | return derived 52 | -------------------------------------------------------------------------------- /functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module that implements APL's primitive functions. 3 | 4 | cf. https://www.jsoftware.com/papers/satn40.htm for complex GCD. 5 | cf. https://www.jsoftware.com/papers/eem/complexfloor.htm for complex floor. 6 | """ 7 | 8 | import functools 9 | import math 10 | 11 | from arraymodel import APLArray, S 12 | 13 | def pervade(func): 14 | """Decorator to define function pervasion into simple scalars.""" 15 | 16 | @functools.wraps(func) 17 | def pervasive_func(*, alpha=None, omega): 18 | # Start by checking if alpha is None 19 | if alpha is None: 20 | if omega.shape: 21 | data = [ 22 | pervasive_func(omega=w, alpha=alpha) for w in omega.data 23 | ] 24 | elif isinstance(omega.data[0], APLArray): 25 | data = [pervasive_func(omega=omega.data[0], alpha=alpha)] 26 | else: 27 | data = [func(omega=omega.data[0], alpha=alpha)] 28 | # Dyadic case from now on 29 | elif alpha.shape and omega.shape: 30 | if alpha.shape != omega.shape: 31 | raise IndexError("Mismatched left and right shapes.") 32 | data = [ 33 | pervasive_func(omega=w, alpha=a) for w, a in zip(omega.data, alpha.data) 34 | ] 35 | elif alpha.shape: 36 | w = omega if omega.is_simple_scalar() else omega.data[0] 37 | data = [pervasive_func(omega=w, alpha=a) for a in alpha.data] 38 | elif omega.shape: 39 | a = alpha if alpha.is_simple_scalar() else alpha.data[0] 40 | data = [pervasive_func(omega=w, alpha=a) for w in omega.data] 41 | # Both alpha and omega are simple scalars 42 | elif alpha.is_simple_scalar() and omega.is_simple_scalar(): 43 | data = [func(omega=omega.data[0], alpha=alpha.data[0])] 44 | else: 45 | a = alpha if alpha.is_simple_scalar() else alpha.data[0] 46 | w = omega if omega.is_simple_scalar() else omega.data[0] 47 | data = pervasive_func(omega=w, alpha=a) 48 | 49 | shape = getattr(alpha, "shape", None) or omega.shape 50 | return APLArray(shape, data) 51 | 52 | return pervasive_func 53 | 54 | def dyadic(op): 55 | """Decorator that ensures that the corresponding APL primitive is called dyadically.""" 56 | 57 | def inner(func): 58 | @functools.wraps(func) 59 | def wrapped(*, alpha=None, omega): 60 | 61 | if alpha is None: 62 | raise SyntaxError(f"{op} is a dyadic function.") 63 | return func(alpha=alpha, omega=omega) 64 | 65 | return wrapped 66 | return inner 67 | 68 | TYPES = { 69 | "boolean": lambda x: x in [0, 1], 70 | int: lambda x: isinstance(x, int), 71 | } 72 | 73 | def arg_types(op, alpha_type=None, omega_type=None): 74 | """Decorator that ensures a scalar APL function receives args with given types.""" 75 | 76 | def inner(func): 77 | @functools.wraps(func) 78 | def wrapped(*, alpha=None, omega): 79 | if (check := TYPES.get(alpha_type)) is not None: 80 | if not check(alpha): 81 | raise TypeError(f"{op} expected a left argument of type {alpha_type}.") 82 | if (check := TYPES.get(omega_type)) is not None: 83 | if not check(omega): 84 | raise TypeError(f"{op} expected a right argument of type {omega_type}.") 85 | return func(alpha=alpha, omega=omega) 86 | 87 | return wrapped 88 | return inner 89 | 90 | def boolean_function(op): 91 | """Decorator that ensures a Boolean function gets passed Boolean arguments.""" 92 | return arg_types(op, "boolean", "boolean") 93 | 94 | @pervade 95 | def plus(*, alpha=None, omega): 96 | """Define monadic complex conjugate and binary addition. 97 | 98 | Monadic case: 99 | + 1 ¯4 5J6 100 | 1 ¯4 5J¯6 101 | Dyadic case: 102 | 1 2 3 + ¯1 5 0J1 103 | 0 7 3J1 104 | """ 105 | 106 | if alpha is None: 107 | return omega.conjugate() 108 | else: 109 | return alpha + omega 110 | 111 | @pervade 112 | def minus(*, alpha=None, omega): 113 | """Define monadic symmetric numbers and dyadic subtraction. 114 | 115 | Monadic case: 116 | - 1 2 ¯3 4J1 117 | ¯1 ¯2 3 ¯4J¯1 118 | Dyadic case: 119 | 1 - 3J0.5 120 | ¯2J¯0.5 121 | """ 122 | 123 | if alpha is None: 124 | alpha = 0 125 | return alpha - omega 126 | 127 | @pervade 128 | def times(*, alpha=None, omega): 129 | """Define monadic sign and dyadic multiplication. 130 | 131 | Monadic case: 132 | × 1 2 0 ¯6 133 | 1 1 0 ¯1 134 | Dyadic case: 135 | 1 2 3 × 0 3 5 136 | 0 6 15 137 | """ 138 | 139 | if alpha is None: 140 | if not omega: 141 | return 0 142 | else: 143 | div = omega/abs(omega) 144 | if not isinstance(omega, complex): 145 | div = round(div) 146 | return div 147 | else: 148 | return alpha*omega 149 | 150 | @pervade 151 | def divide(*, alpha=None, omega): 152 | """Define monadic reciprocal and dyadic division. 153 | 154 | Monadic case: 155 | ÷ 1 ¯2 5J10 156 | 1 ¯0.5 0.04J¯0.08 157 | Dyadic case: 158 | 4 ÷ 3 159 | 1.33333333 160 | """ 161 | 162 | if alpha is None: 163 | alpha = 1 164 | return alpha/omega 165 | 166 | @pervade 167 | def ceiling(*, alpha=None, omega): 168 | """Define monadic ceiling and dyadic max. 169 | 170 | Monadic case: 171 | ⌈ 0.0 1.1 ¯2.3 172 | 0 2 ¯2 173 | Monadic complex ceiling not implemented yet. 174 | Dyadic case: 175 | ¯2 ⌈ 4 176 | 4 177 | """ 178 | 179 | if alpha is None: 180 | if isinstance(alpha, complex): 181 | raise NotImplementedError("Complex ceiling not implemented yet.") 182 | return math.ceil(omega) 183 | else: 184 | return max(alpha, omega) 185 | 186 | @pervade 187 | def floor(*, alpha=None, omega): 188 | """Define monadic floor and dyadic min. 189 | 190 | Monadic case: 191 | ⌊ 0.0 1.1 ¯2.3 192 | 0 1 ¯3 193 | Monadic complex floor not implemented yet. 194 | Dyadic case: 195 | ¯2 ⌊ 4 196 | ¯2 197 | """ 198 | 199 | if alpha is None: 200 | if isinstance(alpha, complex): 201 | raise NotImplementedError("Complex floor not implemented yet.") 202 | return math.floor(omega) 203 | else: 204 | return min(alpha, omega) 205 | 206 | @pervade 207 | @boolean_function("∧") 208 | @dyadic("∧") 209 | def and_(*, alpha=None, omega): 210 | """Define dyadic Boolean and. 211 | 212 | Dyadic case: 213 | 0 0 1 1 ∧ 0 1 0 1 214 | 0 0 0 1 215 | """ 216 | 217 | return alpha and omega 218 | 219 | @pervade 220 | @boolean_function("⍲") 221 | @dyadic("⍲") 222 | def nand(*, alpha=None, omega): 223 | """Define dyadic Boolean nand function. 224 | 225 | Dyadic case: 226 | 0 0 1 1 ⍲ 0 1 0 1 227 | 1 1 1 0 228 | """ 229 | 230 | return 1 - (alpha and omega) 231 | 232 | @pervade 233 | @boolean_function("∨") 234 | @dyadic("∨") 235 | def or_(*, alpha=None, omega): 236 | """Define dyadic Boolean or function. 237 | 238 | Dyadic case: 239 | 0 0 1 1 ∨ 0 1 0 1 240 | 0 1 1 1 241 | """ 242 | 243 | return alpha or omega 244 | 245 | @pervade 246 | @boolean_function("⍱") 247 | @dyadic("⍱") 248 | def nor(*, alpha=None, omega): 249 | """Define dyadic Boolean nor function. 250 | 251 | Dyadic case: 252 | 0 0 1 1 ⍱ 0 1 0 1 253 | 1 0 0 0 254 | """ 255 | 256 | return 1 - (alpha or omega) 257 | 258 | def right_tack(*, alpha=None, omega): 259 | """Define monadic same and dyadic right. 260 | 261 | Monadic case: 262 | ⊢ 3 263 | 3 264 | Dyadic case: 265 | 1 2 3 ⊢ 4 5 6 266 | 4 5 6 267 | """ 268 | 269 | return omega 270 | 271 | def left_tack(*, alpha=None, omega): 272 | """Define monadic same and dyadic left. 273 | 274 | Monadic case: 275 | ⊣ 3 276 | 3 277 | Dyadic case: 278 | 1 2 3 ⊣ 4 5 6 279 | 1 2 3 280 | """ 281 | 282 | return alpha if alpha is not None else omega 283 | 284 | @pervade 285 | def less(*, alpha=None, omega): 286 | """Define dyadic comparison function less than. 287 | 288 | Dyadic case: 289 | 3 < 2 3 4 290 | 0 0 1 291 | """ 292 | 293 | return int(alpha < omega) 294 | 295 | @pervade 296 | def lesseq(*, alpha=None, omega): 297 | """Define dyadic comparison function less than or equal to. 298 | 299 | Dyadic case: 300 | 3 ≤ 2 3 4 301 | 0 1 1 302 | """ 303 | 304 | return int(alpha <= omega) 305 | 306 | @pervade 307 | def eq(*, alpha=None, omega): 308 | """Define dyadic comparison function equal to. 309 | 310 | Dyadic case: 311 | 3 = 2 3 4 312 | 0 1 0 313 | """ 314 | 315 | return int(alpha == omega) 316 | 317 | @pervade 318 | def greatereq(*, alpha=None, omega): 319 | """Define dyadic comparison function greater than or equal to. 320 | 321 | Dyadic case: 322 | 3 ≥ 2 3 4 323 | 1 1 0 324 | """ 325 | 326 | return int(alpha >= omega) 327 | 328 | @pervade 329 | def greater(*, alpha=None, omega): 330 | """Define dyadic comparison function greater than. 331 | 332 | Dyadic case: 333 | 3 > 2 3 4 334 | 1 0 0 335 | """ 336 | 337 | return int(alpha > omega) 338 | 339 | @pervade 340 | def _neq(*, alpha=None, omega): 341 | """Define dyadic comparison function not equal to. 342 | 343 | Dyadic case: 344 | 3 ≠ 2 3 4 345 | 1 0 1 346 | """ 347 | 348 | return int(alpha != omega) 349 | 350 | def _unique_mask(*, alpha=None, omega): 351 | """Define monadic unique mask. 352 | 353 | Monadic case: 354 | ≠ 1 1 2 2 3 3 1 355 | 1 0 1 0 1 0 0 356 | """ 357 | 358 | majors = omega.major_cells() 359 | data = [ 360 | APLArray([], [int(major_cell not in majors.data[:i])]) 361 | for i, major_cell in enumerate(majors.data) 362 | ] 363 | return APLArray([len(data)], data) 364 | 365 | def neq(*, alpha=None, omega): 366 | """Define monadic unique mask and dyadic not equal to. 367 | 368 | Monadic case: 369 | ≠ 1 1 2 2 3 3 1 370 | 1 0 1 0 1 0 0 371 | Dyadic case: 372 | 3 ≠ 2 3 4 373 | 1 0 1 374 | """ 375 | 376 | if alpha is None: 377 | return _unique_mask(alpha=alpha, omega=omega) 378 | else: 379 | return _neq(alpha=alpha, omega=omega) 380 | 381 | def lshoe(*, alpha=None, omega): 382 | """Define monadic and dyadic left shoe. 383 | 384 | Monadic case: 385 | ⊂ 1 2 3 386 | (1 2 3) 387 | ⊂ 1 388 | 1 389 | Dyadic case: 390 | NotImplemented 391 | """ 392 | 393 | if alpha is None: 394 | if omega.is_simple_scalar(): 395 | return omega 396 | else: 397 | return APLArray([], [omega]) 398 | else: 399 | raise NotImplementedError("Partitioned Enclose not implemented yet.") 400 | 401 | @pervade 402 | def _not(*, alpha=None, omega): 403 | """Define monadic not. 404 | 405 | Monadic case: 406 | ~ 1 0 1 0 0 1 0 0 407 | 0 1 0 1 1 0 1 1 408 | """ 409 | 410 | return int(not omega) 411 | 412 | def _without(*, alpha=None, omega): 413 | """Define dyadic without. 414 | 415 | Dyadic case: 416 | 3 1 4 1 5 ~ 1 5 417 | 3 4 418 | (3 2⍴⍳6) ~ 0 1 419 | 2 3 420 | 4 5 421 | """ 422 | 423 | alpha_majors = alpha.major_cells() 424 | needle_rank = len(alpha.shape) - 1 425 | if needle_rank > len(omega.shape): 426 | raise ValueError(f"Right argument to ~ needs rank at least {needle_rank}.") 427 | # Get a list with the arrays that we wish to exclude from the left. 428 | haystack = omega.n_cells(needle_rank).data 429 | 430 | newdata = [] 431 | count = 0 432 | for major in alpha_majors.data: 433 | if major not in haystack: 434 | if major.is_scalar(): 435 | newdata.append(major) 436 | else: 437 | newdata += major.data 438 | count += 1 439 | newshape = [count] + alpha.shape[1:] 440 | return APLArray(newshape, newdata) 441 | 442 | def without(*, alpha=None, omega): 443 | """Define monadic not and dyadic without. 444 | 445 | Monadic case: 446 | ~ 1 0 1 0 0 1 0 0 447 | 0 1 0 1 1 0 1 1 448 | Dyadic case: 449 | 3 1 4 1 5 ~ 1 5 450 | 3 4 451 | """ 452 | 453 | if alpha is None: 454 | return _not(alpha=alpha, omega=omega) 455 | else: 456 | return _without(alpha=alpha, omega=omega) 457 | 458 | def _index_generator(*, alpha=None, omega): 459 | """Define monadic Index Generator. 460 | 461 | Monadic case: 462 | ⍳ 4 463 | 0 1 2 3 464 | """ 465 | 466 | if (r := len(omega.shape)) > 1: 467 | raise ValueError(f"Index generator did not expect array of rank {r}.") 468 | 469 | if omega.is_scalar() and isinstance(omega.data[0], int): 470 | shape = [omega.data[0]] 471 | else: 472 | # If omega is not a scalar, then we want the integers that compose the vector. 473 | shape = [elem.data[0] for elem in omega.data] 474 | 475 | # Check the argument to index generator is only non-negative integers. 476 | if any(not isinstance(dim, int) for dim in shape): 477 | raise TypeError(f"Cannot generate indices with non-integers {shape}.") 478 | elif any(dim < 0 for dim in shape): 479 | raise ValueError("Cannot generate indices with negative integers.") 480 | 481 | decoded = map(lambda n: _encode(shape, n), range(math.prod(shape))) 482 | if omega.is_scalar(): 483 | data = [S(d[0]) for d in decoded] 484 | else: 485 | r = len(shape) 486 | data = [APLArray([r], list(map(S, d))) for d in decoded] 487 | return APLArray(shape, data) 488 | 489 | def iota(*, alpha=None, omega): 490 | """Define monadic index generator and dyadic index of. 491 | 492 | Monadic case: 493 | ⍳ 4 494 | 0 1 2 3 495 | Dyadic case: 496 | 6 5 32 4 ⍳ 32 497 | 2 498 | """ 499 | 500 | if alpha is None: 501 | return _index_generator(alpha=alpha, omega=omega) 502 | else: 503 | raise NotImplementedError("Index Of not implemented yet.") 504 | 505 | def rho(*, alpha=None, omega): 506 | """Define monadic shape and dyadic reshape. 507 | 508 | Monadic case: 509 | ⍴ ⍳2 3 510 | 2 3 511 | Dyadic case: 512 | 3⍴⊂1 2 513 | (1 2)(1 2)(1 2) 514 | """ 515 | 516 | if alpha is None: 517 | shape = [len(omega.shape)] 518 | data = [S(i) for i in omega.shape] 519 | return APLArray(shape, data) 520 | else: 521 | rank = len(alpha.shape) 522 | if rank > 1: 523 | raise ValueError(f"Left argument of reshape cannot have rank {rank}.") 524 | 525 | if alpha.is_scalar(): 526 | shape = [alpha.data[0]] 527 | else: 528 | shape = [d.data[0] for d in alpha.data] 529 | 530 | if not all(isinstance(i, int) for i in shape): 531 | raise TypeError("Left argument of reshape expects integers.") 532 | 533 | data_from = omega.data if len(omega.shape) > 0 else [omega] 534 | # Extend the data roughly if needed, then truncate if needed. 535 | data = data_from*(math.ceil(math.prod(shape)/len(data_from))) 536 | data = data[:math.prod(shape)] 537 | return APLArray(shape, data) 538 | 539 | def _decode(alpha, omega): 540 | """Helper function that decodes one APLArray w.r.t. to another. 541 | 542 | Notice that this goes against the _encode helper function, that 543 | _does not_ deal with APLArray objects. 544 | """ 545 | 546 | acc = 0 547 | acc_prod = 1 548 | alphas = [a.data[0] for a in alpha.data] 549 | omegas = [o.data[0] for o in omega.data] 550 | for a, o in zip(alphas[::-1], omegas[::-1]): 551 | acc += acc_prod * o 552 | acc_prod *= a 553 | return S(acc) 554 | 555 | @dyadic("⊥") 556 | def decode(*, alpha=None, omega): 557 | """Define dyadic decode. 558 | 559 | Dyadic case: 560 | 2 ⊥ 1 1 0 1 561 | 13 562 | 24 60 60 ⊥ 2 46 40 563 | 10000 564 | """ 565 | 566 | # Compute the final shape now, then promote omega for ease of computation. 567 | final_shape = alpha.shape[:-1] + omega.shape[1:] 568 | omega = omega.at_least_vector() 569 | 570 | # Ensure alpha has the correct shape: 571 | if alpha.is_simple_scalar() or alpha.shape == [1]: 572 | alpha = rho(alpha=S(omega.shape[0]), omega=alpha) 573 | 574 | # Ensure omega has the correct leading dimension: 575 | if omega.shape[0] != alpha.shape[-1]: 576 | if omega.shape[0] != 1: 577 | raise IndexError("Trailing dimension of ⍺ should match leading dimension of ⍵ in ⍺⊥⍵.") 578 | target_shape_values = [S(v) for v in [alpha.shape[-1]]+omega.shape[1:]] 579 | target_shape = APLArray([len(omega.shape)], target_shape_values) 580 | omega = rho(alpha=target_shape, omega=omega) 581 | 582 | dist = math.prod(omega.shape[1:]) 583 | omega_first_axis_enclosure = APLArray( 584 | omega.shape[1:], 585 | [APLArray([omega.shape[0]], omega.data[i::dist]) for i in range(dist)] 586 | ) 587 | # Pair each 1-cell of alpha with each element in the first axis enclosure of omega. 588 | data = [_decode(a, o) for a in alpha.n_cells(1).data for o in omega_first_axis_enclosure.data] 589 | # Check if we should return a container array or just a single simple scalar. 590 | return APLArray(final_shape, data) if final_shape else data[0] 591 | 592 | def _encode(radices, n): 593 | """Helper function to the encode ⊤ primitive. 594 | 595 | Takes a list of radices and a simple scalar n. 596 | (Notice that `radices` and `n` are _not_ APLArray objects) 597 | 598 | E.g. (10000 seconds is 2h 46min 40s): 599 | 24 60 60 ⊤ 10000 600 | 2 46 40 601 | """ 602 | 603 | n = n % (math.prod(radices) if 0 not in radices else n + 1) 604 | bs = [] 605 | for m in radices[::-1]: 606 | n, b = divmod(n, m) if m != 0 else (0, n) 607 | bs.append(b) 608 | return bs[::-1] 609 | 610 | @dyadic("⊤") 611 | def encode(*, alpha=None, omega): 612 | """Define dyadic encode. 613 | 614 | Dyadic case: 615 | 2 3 4 ⊤ 23 24 616 | 1 0 617 | 2 0 618 | 3 0 619 | 620 | Notice that alpha has the radices along its first dimension. 621 | Therefore, the first axis enclosure of alpha gives vectors with the radices. 622 | The final result has the encodings also along the first axis, 623 | so the first axis enclosure of the result is easy to relate to the first axis 624 | enclosure of alpha and the original omega. 625 | """ 626 | 627 | # Compute the resulting shape now and promote alpha for ease of calculations. 628 | result_shape = alpha.shape + omega.shape 629 | alpha = alpha.at_least_vector() 630 | result_data = [0]*math.prod(result_shape) 631 | # Radices come from alpha.shape[i::dist] (~ the first axis enclosure of alpha). 632 | dist = math.prod(alpha.shape[1:]) 633 | # Resulting vectors go to result_data[j::rdist] (~ the first axis enclosure of the result). 634 | rdist = math.prod(result_shape[1:]) 635 | for i in range(dist): 636 | radices = [s.data[0] for s in alpha.data[i::dist]] 637 | for j, s in enumerate(omega.at_least_vector().data): 638 | result_data[i*math.prod(omega.shape)+j::rdist] = map(S, _encode(radices, s.data[0])) 639 | 640 | return APLArray(result_shape, result_data) if result_shape else result_data[0] 641 | -------------------------------------------------------------------------------- /moperators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module that implements APL's monadic operators. 3 | """ 4 | 5 | import math 6 | 7 | from arraymodel import APLArray 8 | 9 | def commute(*, aalpha): 10 | """Define the monadic commute ⍨ operator. 11 | 12 | Monadic case: 13 | f⍨ ⍵ 14 | ⍵ f ⍵ 15 | Dyadic case: 16 | ⍺ f⍨ ⍵ 17 | ⍵ f ⍺ 18 | """ 19 | 20 | def derived(*, alpha=None, omega): 21 | alpha = omega if alpha is None else alpha 22 | return aalpha(alpha=omega, omega=alpha) 23 | return derived 24 | 25 | def diaeresis(*, aalpha): 26 | """Define the monadic diaeresis ¨ operator. 27 | 28 | Monadic case: 29 | f¨ x y z 30 | (f x) (f y) (f z) 31 | Dyadic case: 32 | ⍺ f¨ x y z 33 | (⍺ f x) (⍺ f y) (⍺ f z) 34 | x y z f¨ ⍵ 35 | (x f ⍵) (y f ⍵) (z f ⍵) 36 | a b c f¨ x y z 37 | (a f x) (b f y) (c f z) 38 | """ 39 | 40 | def derived(*, alpha=None, omega): 41 | if alpha: 42 | if alpha.shape and omega.shape: 43 | if len(alpha.shape) != len(omega.shape): 44 | raise ValueError("Mismatched ranks of left and right arguments.") 45 | elif alpha.shape and omega.shape and alpha.shape != omega.shape: 46 | raise IndexError("Left and right arguments must have the same dimensions.") 47 | shape = alpha.shape or omega.shape 48 | else: 49 | shape = omega.shape 50 | 51 | l = math.prod(shape) 52 | omegas = omega.data if omega.shape else l*[omega] 53 | alphas = l*[None] if alpha is None else ( 54 | alpha.data if alpha.shape else l*[alpha] 55 | ) 56 | data = [aalpha(omega=o, alpha=a) for o, a in zip(omegas, alphas)] 57 | if not shape: 58 | data = data[0] 59 | return APLArray(shape, data) 60 | return derived 61 | -------------------------------------------------------------------------------- /rgspl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implement a subset of the APL programming language. 3 | 4 | Supports the monadic/dyadic functions +-×÷⌈⌊⊢⊣⍳<≤=≥>≠~⊂ ; 5 | Supports (negative) integers/floats/complex numbers and vectors of those ; 6 | Supports the monadic operators ⍨ and ¨ ; 7 | Supports the dyadic operators ∘ (only functions as operands) and ⍥ ; 8 | Supports parenthesized expressions ; 9 | Supports multiple expressions separated by ⋄ ; 10 | Supports comments with ⍝ ; 11 | 12 | This is the grammar supported: 13 | 14 | program ::= EOF statement_list 15 | statement_list ::= (statement "⋄")* statement 16 | statement ::= ( ID "←" | vector function | function )* vector 17 | function ::= function mop | function dop f | f 18 | dop ::= "∘" | "⍤" | "⍥" 19 | mop ::= "⍨" | "¨" 20 | f ::= "+" | "-" | "×" | "÷" | "⌈" | "⌊" | 21 | | "⊢" | "⊣" | "⍳" | "<" | "≤" | "=" | 22 | | "≥" | ">" | "≠" | "~" | "⊂" | "⍴" | 23 | | "∧" | "∨" | "⍲" | "⍱" | "⊥" | "⊤" | LPARENS function RPARENS 24 | vector ::= vector* ( scalar | ( LPARENS statement RPARENS ) ) 25 | scalar ::= INTEGER | FLOAT | COMPLEX | ID 26 | """ 27 | # pylint: disable=invalid-name 28 | 29 | import argparse 30 | import traceback 31 | from typing import List 32 | 33 | import doperators 34 | import functions 35 | import moperators 36 | from arraymodel import APLArray 37 | 38 | class Token: 39 | """Represents a token parsed from the source code.""" 40 | 41 | # "Data types" 42 | INTEGER = "INTEGER" 43 | FLOAT = "FLOAT" 44 | COMPLEX = "COMPLEX" 45 | ID = "ID" 46 | # Functions 47 | PLUS = "PLUS" 48 | MINUS = "MINUS" 49 | TIMES = "TIMES" 50 | DIVIDE = "DIVIDE" 51 | CEILING = "CEILING" 52 | FLOOR = "FLOOR" 53 | RIGHT_TACK = "RIGHT_TACK" 54 | LEFT_TACK = "LEFT_TACK" 55 | IOTA = "IOTA" 56 | LESS = "LESS" 57 | LESSEQ = "LESSEQ" 58 | EQ = "EQ" 59 | GREATEREQ = "GREATEREQ" 60 | GREATER = "GREATER" 61 | NEQ = "NEQ" 62 | WITHOUT = "WITHOUT" 63 | LSHOE = "LSHOE" 64 | RHO = "RHO" 65 | AND = "AND_" 66 | OR = "OR_" 67 | NAND = "NAND" 68 | NOR = "NOR" 69 | DECODE = "DECODE" 70 | ENCODE = "ENCODE" 71 | # Operators 72 | COMMUTE = "COMMUTE" 73 | DIAERESIS = "DIAERESIS" 74 | JOT = "JOT" 75 | ATOP = "ATOP" 76 | OVER = "OVER" 77 | # Misc 78 | DIAMOND = "DIAMOND" 79 | NEGATE = "NEGATE" 80 | ASSIGNMENT = "ASSIGNMENT" 81 | LPARENS = "LPARENS" 82 | RPARENS = "RPARENS" 83 | EOF = "EOF" 84 | 85 | # Helpful lists of token types. 86 | FUNCTIONS = [ 87 | PLUS, MINUS, TIMES, DIVIDE, FLOOR, CEILING, RIGHT_TACK, LEFT_TACK, IOTA, 88 | LESS, LESSEQ, EQ, GREATEREQ, GREATER, NEQ, WITHOUT, LSHOE, RHO, AND, OR, 89 | NAND, NOR, DECODE, ENCODE, 90 | ] 91 | MONADIC_OPS = [COMMUTE, DIAERESIS] 92 | DYADIC_OPS = [JOT, ATOP, OVER] 93 | # Tokens that could be inside an array. 94 | ARRAY_TOKENS = [INTEGER, FLOAT, COMPLEX, ID] 95 | 96 | # What You See Is What You Get characters that correspond to tokens. 97 | # The mapping from characteres to token types. 98 | WYSIWYG_MAPPING = { 99 | "+": PLUS, 100 | "-": MINUS, 101 | "×": TIMES, 102 | "÷": DIVIDE, 103 | "⌈": CEILING, 104 | "⌊": FLOOR, 105 | "⊢": RIGHT_TACK, 106 | "⊣": LEFT_TACK, 107 | "⍳": IOTA, 108 | "<": LESS, 109 | "≤": LESSEQ, 110 | "=": EQ, 111 | "≥": GREATEREQ, 112 | ">": GREATER, 113 | "≠": NEQ, 114 | "~": WITHOUT, 115 | "⊂": LSHOE, 116 | "⍴": RHO, 117 | "∧": AND, 118 | "∨": OR, 119 | "⍲": NAND, 120 | "⍱": NOR, 121 | "⊥": DECODE, 122 | "⊤": ENCODE, 123 | "⍨": COMMUTE, 124 | "¨": DIAERESIS, 125 | "∘": JOT, 126 | "⍤": ATOP, 127 | "⍥": OVER, 128 | "←": ASSIGNMENT, 129 | "(": LPARENS, 130 | ")": RPARENS, 131 | "⋄": DIAMOND, 132 | "\n": DIAMOND, 133 | } 134 | 135 | ID_CHARS = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 136 | 137 | def __init__(self, type_, value): 138 | self.type = type_ 139 | self.value = value 140 | 141 | def __str__(self): 142 | return f"Token({self.type}, {self.value})" 143 | 144 | def __repr__(self): 145 | return self.__str__() 146 | 147 | def __eq__(self, other): 148 | return ( 149 | isinstance(other, Token) 150 | and (self.type, self.value) == (other.type, other.value) 151 | ) 152 | 153 | 154 | class Tokenizer: 155 | """Class that tokenizes source code into tokens.""" 156 | 157 | def __init__(self, code): 158 | self.code = code 159 | self.pos = 0 160 | self.current_char = self.code[self.pos] 161 | 162 | def error(self, message): 163 | """Raises a Tokenizer error.""" 164 | raise Exception(f"TokenizerError: {message}") 165 | 166 | def advance(self): 167 | """Advances the cursor position and sets the current character.""" 168 | 169 | self.pos += 1 170 | self.current_char = None if self.pos >= len(self.code) else self.code[self.pos] 171 | 172 | def skip_whitespace(self): 173 | """Skips all the whitespace in the source code.""" 174 | 175 | while self.current_char and self.current_char in " \t": 176 | self.advance() 177 | 178 | def skip_comment(self): 179 | """Skips commented code.""" 180 | 181 | if not self.current_char == "⍝": 182 | return 183 | while self.current_char and self.current_char != "\n": 184 | self.advance() 185 | 186 | def get_integer(self): 187 | """Parses an integer from the source code.""" 188 | 189 | start_idx = self.pos 190 | while self.current_char and self.current_char.isdigit(): 191 | self.advance() 192 | return self.code[start_idx:self.pos] or "0" 193 | 194 | def get_real_number(self): 195 | """Parses a real number from the source code.""" 196 | 197 | # Check for a negation of the number. 198 | if self.current_char == "¯": 199 | self.advance() 200 | int_ = "-" + self.get_integer() 201 | else: 202 | int_ = self.get_integer() 203 | # Check if we have a decimal number here. 204 | if self.current_char == ".": 205 | self.advance() 206 | dec_ = self.get_integer() 207 | else: 208 | dec_ = "0" 209 | 210 | if int(dec_): 211 | return float(f"{int_}.{dec_}") 212 | else: 213 | return int(int_) 214 | 215 | def get_number_token(self): 216 | """Parses a number token from the source code.""" 217 | 218 | real = self.get_real_number() 219 | if self.current_char == "J": 220 | self.advance() 221 | im = self.get_real_number() 222 | else: 223 | im = 0 224 | 225 | if im: 226 | tok = Token(Token.COMPLEX, complex(real, im)) 227 | elif isinstance(real, int): 228 | tok = Token(Token.INTEGER, real) 229 | elif isinstance(real, float): 230 | tok = Token(Token.FLOAT, real) 231 | else: 232 | self.error("Cannot recognize type of number.") 233 | return tok 234 | 235 | def get_id_token(self): 236 | """Retrieves an identifier token.""" 237 | 238 | start = self.pos 239 | while self.current_char and self.current_char in Token.ID_CHARS: 240 | self.advance() 241 | return Token(Token.ID, self.code[start:self.pos]) 242 | 243 | def get_wysiwyg_token(self): 244 | """Retrieves a WYSIWYG token.""" 245 | 246 | char = self.current_char 247 | self.advance() 248 | try: 249 | return Token(Token.WYSIWYG_MAPPING[char], char) 250 | except KeyError: 251 | self.error("Could not parse WYSIWYG token.") 252 | 253 | def get_next_token(self): 254 | """Finds the next token in the source code.""" 255 | 256 | self.skip_whitespace() 257 | self.skip_comment() 258 | if not self.current_char: 259 | return Token(Token.EOF, None) 260 | 261 | if self.current_char in "¯.0123456789": 262 | return self.get_number_token() 263 | 264 | if self.current_char in Token.ID_CHARS: 265 | return self.get_id_token() 266 | 267 | if self.current_char in Token.WYSIWYG_MAPPING: 268 | return self.get_wysiwyg_token() 269 | 270 | self.error("Could not parse the next token...") 271 | 272 | def tokenize(self): 273 | """Returns the whole token list.""" 274 | 275 | tokens = [self.get_next_token()] 276 | while tokens[-1].type != Token.EOF: 277 | tokens.append(self.get_next_token()) 278 | # Move the EOF token to the beginning of the list. 279 | return [tokens[-1]] + tokens[:-1] 280 | 281 | 282 | class ASTNode: 283 | """Stub class to be inherited by the different types of AST nodes. 284 | 285 | The AST Nodes are used by the Parser instances to build an 286 | Abstract Syntax Tree out of the APL programs. 287 | These ASTs can then be traversed to interpret an APL program. 288 | """ 289 | 290 | def __repr__(self): 291 | return self.__str__() 292 | 293 | 294 | class S(ASTNode): 295 | """Node for a simple scalar like 3 or ¯4.2""" 296 | def __init__(self, token: Token): 297 | self.token = token 298 | self.value = self.token.value 299 | 300 | def __str__(self): 301 | return f"S({self.value})" 302 | 303 | 304 | class V(ASTNode): 305 | """Node for a stranded vector of simple scalars, like 3 ¯4 5.6""" 306 | def __init__(self, children: List[ASTNode]): 307 | self.children = children 308 | 309 | def __str__(self): 310 | return f"V({self.children})" 311 | 312 | 313 | class MOp(ASTNode): 314 | """Node for monadic operators like ⍨""" 315 | def __init__(self, token: Token, child: ASTNode): 316 | self.token = token 317 | self.operator = self.token.value 318 | self.child = child 319 | 320 | def __str__(self): 321 | return f"MOp({self.operator} {self.child})" 322 | 323 | 324 | class DOp(ASTNode): 325 | """Node for dyadic operators like ∘""" 326 | def __init__(self, token: Token, left: ASTNode, right: ASTNode): 327 | self.token = token 328 | self.operator = self.token.value 329 | self.left = left 330 | self.right = right 331 | 332 | def __str__(self): 333 | return f"DOP({self.left} {self.operator} {self.right})" 334 | 335 | 336 | class F(ASTNode): 337 | """Node for built-in functions like + or ⌈""" 338 | def __init__(self, token: Token): 339 | self.token = token 340 | self.function = self.token.value 341 | 342 | def __str__(self): 343 | return f"F({self.function})" 344 | 345 | 346 | class Monad(ASTNode): 347 | """Node for monadic function calls.""" 348 | def __init__(self, function: ASTNode, omega: ASTNode): 349 | self.function = function 350 | self.omega = omega 351 | 352 | def __str__(self): 353 | return f"Monad({self.function} {self.omega})" 354 | 355 | 356 | class Dyad(ASTNode): 357 | """Node for dyadic functions.""" 358 | def __init__(self, function: ASTNode, alpha: ASTNode, omega: ASTNode): 359 | self.function = function 360 | self.alpha = alpha 361 | self.omega = omega 362 | 363 | def __str__(self): 364 | return f"Dyad({self.function} {self.alpha} {self.omega})" 365 | 366 | 367 | class Assignment(ASTNode): 368 | """Node for assignment expressions.""" 369 | def __init__(self, varname: ASTNode, value: ASTNode): 370 | self.varname = varname 371 | self.value = value 372 | 373 | def __str__(self): 374 | return f"Assignment({self.varname.token.value} ← {self.value})" 375 | 376 | 377 | class Var(ASTNode): 378 | """Node for variable references.""" 379 | def __init__(self, token: Token): 380 | self.token = token 381 | self.name = self.token.value 382 | 383 | def __str__(self): 384 | return f"Var({self.token.value})" 385 | 386 | 387 | class Statements(ASTNode): 388 | """Node to represent a series of consecutive statements.""" 389 | def __init__(self): 390 | self.children = [] 391 | 392 | def __str__(self): 393 | return str(self.children) 394 | 395 | 396 | class Parser: 397 | """Implements a parser for a subset of the APL language. 398 | 399 | The grammar parsed is available at the module-level docstring. 400 | """ 401 | 402 | def __init__(self, tokenizer, debug=False): 403 | self.tokens = tokenizer.tokenize() 404 | self.pos = len(self.tokens) - 1 405 | self.token_at = self.tokens[self.pos] 406 | self.debug_on = debug 407 | 408 | def debug(self, message): 409 | """If the debugging option is on, print a message.""" 410 | if self.debug_on: 411 | print(f"PD @ {message}") 412 | 413 | def error(self, message): 414 | """Throws a Parser-specific error message.""" 415 | raise Exception(f"Parser: {message}") 416 | 417 | def eat(self, token_type): 418 | """Checks if the current token matches the expected token type.""" 419 | 420 | if self.token_at.type != token_type: 421 | self.error(f"Expected {token_type} and got {self.token_at.type}.") 422 | else: 423 | self.pos -= 1 424 | self.token_at = None if self.pos < 0 else self.tokens[self.pos] 425 | 426 | def peek(self): 427 | """Returns the next token type without consuming it.""" 428 | peek_at = self.pos - 1 429 | return None if peek_at < 0 else self.tokens[peek_at].type 430 | 431 | def peek_beyond_parens(self): 432 | """Returns the next token type that is not a right parenthesis.""" 433 | peek_at = self.pos - 1 434 | while peek_at >= 0 and self.tokens[peek_at].type == Token.RPARENS: 435 | peek_at -= 1 436 | return None if peek_at < 0 else self.tokens[peek_at].type 437 | 438 | def parse_program(self): 439 | """Parses a full program.""" 440 | 441 | self.debug(f"Parsing program from {self.tokens}") 442 | statement_list = self.parse_statement_list() 443 | self.eat(Token.EOF) 444 | return statement_list 445 | 446 | def parse_statement_list(self): 447 | """Parses a list of statements.""" 448 | 449 | self.debug(f"Parsing a statement list from {self.tokens}") 450 | root = Statements() 451 | statements = [self.parse_statement()] 452 | while self.token_at.type == Token.DIAMOND: 453 | self.eat(Token.DIAMOND) 454 | statements.append(self.parse_statement()) 455 | 456 | root.children = statements 457 | return root 458 | 459 | def parse_statement(self): 460 | """Parses a statement.""" 461 | 462 | self.debug(f"Parsing statement from {self.tokens[:self.pos+1]}") 463 | 464 | relevant_types = [Token.ASSIGNMENT, Token.RPARENS] + Token.FUNCTIONS + Token.MONADIC_OPS 465 | statement = self.parse_vector() 466 | while self.token_at.type in relevant_types: 467 | if self.token_at.type == Token.ASSIGNMENT: 468 | self.eat(Token.ASSIGNMENT) 469 | statement = Assignment(Var(self.token_at), statement) 470 | self.eat(Token.ID) 471 | else: 472 | function = self.parse_function() 473 | if self.token_at.type in [Token.RPARENS] + Token.ARRAY_TOKENS: 474 | array = self.parse_vector() 475 | statement = Dyad(function, array, statement) 476 | else: 477 | statement = Monad(function, statement) 478 | 479 | return statement 480 | 481 | def parse_vector(self): 482 | """Parses a vector composed of possibly several simple scalars.""" 483 | 484 | self.debug(f"Parsing vector from {self.tokens[:self.pos+1]}") 485 | 486 | nodes = [] 487 | while self.token_at.type in Token.ARRAY_TOKENS + [Token.RPARENS]: 488 | if self.token_at.type == Token.RPARENS: 489 | if self.peek_beyond_parens() in Token.ARRAY_TOKENS: 490 | self.eat(Token.RPARENS) 491 | nodes.append(self.parse_statement()) 492 | self.eat(Token.LPARENS) 493 | else: 494 | break 495 | else: 496 | nodes.append(self.parse_scalar()) 497 | nodes = nodes[::-1] 498 | if not nodes: 499 | self.error("Failed to parse scalars inside a vector.") 500 | elif len(nodes) == 1: 501 | node = nodes[0] 502 | else: 503 | node = V(nodes) 504 | return node 505 | 506 | def parse_scalar(self): 507 | """Parses a simple scalar.""" 508 | 509 | self.debug(f"Parsing scalar from {self.tokens[:self.pos+1]}") 510 | 511 | if self.token_at.type == Token.ID: 512 | scalar = Var(self.token_at) 513 | self.eat(Token.ID) 514 | elif self.token_at.type == Token.INTEGER: 515 | scalar = S(self.token_at) 516 | self.eat(Token.INTEGER) 517 | elif self.token_at.type == Token.FLOAT: 518 | scalar = S(self.token_at) 519 | self.eat(Token.FLOAT) 520 | else: 521 | scalar = S(self.token_at) 522 | self.eat(Token.COMPLEX) 523 | 524 | return scalar 525 | 526 | def parse_function(self): 527 | """Parses a (derived) function.""" 528 | 529 | self.debug(f"Parsing function from {self.tokens[:self.pos+1]}") 530 | 531 | if self.token_at.type in Token.MONADIC_OPS: 532 | function = self.parse_mop() 533 | function.child = self.parse_function() 534 | else: 535 | function = self.parse_f() 536 | if self.token_at.type in Token.DYADIC_OPS: 537 | dop = DOp(self.token_at, None, function) 538 | self.eat(dop.token.type) 539 | dop.left = self.parse_function() 540 | function = dop 541 | return function 542 | 543 | def parse_mop(self): 544 | """Parses a monadic operator.""" 545 | 546 | self.debug(f"Parsing a mop from {self.tokens[:self.pos+1]}") 547 | 548 | mop = MOp(self.token_at, None) 549 | if (t := self.token_at.type) not in Token.MONADIC_OPS: 550 | self.error(f"{t} is not a valid monadic operator.") 551 | self.eat(t) 552 | 553 | return mop 554 | 555 | def parse_f(self): 556 | """Parses a simple one-character function.""" 557 | 558 | self.debug(f"Parsing f from {self.tokens[:self.pos+1]}") 559 | 560 | if (t := self.token_at.type) in Token.FUNCTIONS: 561 | f = F(self.token_at) 562 | self.eat(t) 563 | else: 564 | self.eat(Token.RPARENS) 565 | f = self.parse_function() 566 | self.eat(Token.LPARENS) 567 | 568 | return f 569 | 570 | def parse(self): 571 | """Parses the whole AST.""" 572 | return self.parse_program() 573 | 574 | 575 | class NodeVisitor: 576 | """Base class for the node visitor pattern.""" 577 | def visit(self, node): 578 | """Dispatches the visit call to the appropriate function.""" 579 | method_name = f"visit_{type(node).__name__}" 580 | visitor = getattr(self, method_name, self.generic_visit) 581 | return visitor(node) 582 | 583 | def generic_visit(self, node): 584 | """Default method for unknown nodes.""" 585 | raise Exception(f"No visit method for {type(node).__name__}") 586 | 587 | class Interpreter(NodeVisitor): 588 | """APL interpreter using the visitor pattern.""" 589 | 590 | def __init__(self, parser): 591 | self.parser = parser 592 | self.var_lookup = {} 593 | 594 | def visit_S(self, scalar): 595 | """Returns the value of a scalar.""" 596 | return APLArray([], [scalar.value]) 597 | 598 | def visit_V(self, array): 599 | """Returns the value of an array.""" 600 | scalars = [self.visit(child) for child in array.children] 601 | return APLArray([len(scalars)], scalars) 602 | 603 | def visit_Var(self, var): 604 | """Tries to fetch the value of a variable.""" 605 | return self.var_lookup[var.name] 606 | 607 | def visit_Statements(self, statements): 608 | """Visits each statement in order.""" 609 | return [self.visit(child) for child in statements.children[::-1]][-1] 610 | 611 | def visit_Assignment(self, assignment): 612 | """Assigns a value to a variable.""" 613 | 614 | value = self.visit(assignment.value) 615 | varname = assignment.varname.name 616 | self.var_lookup[varname] = value 617 | return value 618 | 619 | def visit_Monad(self, monad): 620 | """Evaluate the function on its only argument.""" 621 | 622 | function = self.visit(monad.function) 623 | omega = self.visit(monad.omega) 624 | return function(omega=omega) 625 | 626 | def visit_Dyad(self, dyad): 627 | """Evaluate a dyad on both its arguments.""" 628 | 629 | function = self.visit(dyad.function) 630 | omega = self.visit(dyad.omega) 631 | alpha = self.visit(dyad.alpha) 632 | return function(alpha=alpha, omega=omega) 633 | 634 | def visit_F(self, func): 635 | """Fetch the callable function.""" 636 | 637 | name = func.token.type.lower() 638 | function = getattr(functions, name, None) 639 | if function is None: 640 | raise Exception(f"Could not find function {name}.") 641 | return function 642 | 643 | def visit_MOp(self, mop): 644 | """Fetch the operand and alter it.""" 645 | 646 | aalpha = self.visit(mop.child) 647 | name = mop.token.type.lower() 648 | operator = getattr(moperators, name, None) 649 | if operator is None: 650 | raise Exception(f"Could not find monadic operator {name}.") 651 | return operator(aalpha=aalpha) 652 | 653 | def visit_DOp(self, dop): 654 | """Fetch the operands and alter them as needed.""" 655 | 656 | oomega = self.visit(dop.right) 657 | aalpha = self.visit(dop.left) 658 | name = dop.token.type.lower() 659 | operator = getattr(doperators, name, None) 660 | if operator is None: 661 | raise Exception(f"Could not find dyadic operator {name}.") 662 | return operator(aalpha=aalpha, oomega=oomega) 663 | 664 | def interpret(self): 665 | """Interpret the APL code the parser was given.""" 666 | tree = self.parser.parse() 667 | return self.visit(tree) 668 | 669 | if __name__ == "__main__": 670 | 671 | arg_parser = argparse.ArgumentParser(description="Parse and interpret an APL program.") 672 | arg_parser.add_argument("-d", "--debug", action="store_true") 673 | main_group = arg_parser.add_mutually_exclusive_group() 674 | main_group.add_argument( 675 | "--repl", 676 | action="store_true", 677 | help="starts a REPL session", 678 | ) 679 | main_group.add_argument( 680 | "-f", 681 | "--file", 682 | nargs=1, 683 | metavar="filename", 684 | help="filename with code to parse and interpret", 685 | type=str, 686 | ) 687 | main_group.add_argument( 688 | "-c", 689 | "--code", 690 | nargs="+", 691 | metavar="expression", 692 | help="expression(s) to be interpreted", 693 | type=str, 694 | ) 695 | 696 | args = arg_parser.parse_args() 697 | 698 | if args.repl: 699 | print("Please notice that, from one input line to the next, variables aren't stored (yet).") 700 | while inp := input(" >> "): 701 | try: 702 | print(Interpreter(Parser(Tokenizer(inp), debug=args.debug)).interpret()) 703 | except Exception as error: 704 | traceback.print_exc() 705 | 706 | elif args.code: 707 | for expr in args.code: 708 | print(f"{expr} :") 709 | print(Interpreter(Parser(Tokenizer(expr), debug=args.debug)).interpret()) 710 | 711 | elif args.file: 712 | print("Not implemented yet...") 713 | 714 | else: 715 | arg_parser.print_usage() 716 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # RGSPL Unit Tests 2 | 3 | To run the tests type `python -m unittest discover -s tests` from the top level directory. 4 | -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the evaluation of the basic data types and arrays of those. 3 | """ 4 | 5 | import unittest 6 | 7 | from arraymodel import APLArray 8 | from utils import APLTestCase, S 9 | 10 | class TestScalarEvaluation(APLTestCase): 11 | """Test that scalars are evaluated conveniently.""" 12 | 13 | def test_nonneg_integers(self): 14 | self.assertEqual("0", S(0)) 15 | self.assertEqual("1", S(1)) 16 | self.assertEqual("2", S(2)) 17 | self.assertEqual("123456789987654321", S(123456789987654321)) 18 | 19 | def test_negative_integers(self): 20 | self.assertEqual("¯1", S(-1)) 21 | self.assertEqual("¯2", S(-2)) 22 | self.assertEqual("¯0", S(0)) 23 | self.assertEqual("¯973", S(-973)) 24 | 25 | def test_floats(self): 26 | self.assertEqual("0.0", S(0)) 27 | self.assertEqual("0.5", S(0.5)) 28 | self.assertEqual("¯0.25", S(-0.25)) 29 | self.assertEqual("¯0.125", S(-0.125)) 30 | 31 | def test_complex_numbers(self): 32 | self.assertEqual("0J0", S(0)) 33 | self.assertEqual("1J1", S(1 + 1j)) 34 | self.assertEqual("3J¯0.5", S(3 - 0.5j)) 35 | self.assertEqual("¯0.3J15", S(-0.3 + 15j)) 36 | 37 | def test_redundant_parens(self): 38 | self.assertEqual("(0.2)", S(0.2)) 39 | self.assertEqual("(((¯3)))", S(-3)) 40 | self.assertEqual("((((((((3J4))))))))", S(3 + 4j)) 41 | 42 | class TestArrayEvaluation(APLTestCase): 43 | """Test that arrays (that are not scalars) are evaluated conveniently.""" 44 | 45 | def test_simple_arrays(self): 46 | self.assertEqual("1 2 3 4", APLArray([4], [S(1), S(2), S(3), S(4)])) 47 | self.assertEqual("¯1 1 ¯1 1", APLArray([4], [S(-1), S(1), S(-1), S(1)])) 48 | self.assertEqual("(2.3 5.6 7.8 9)", APLArray([4], [S(2.3), S(5.6), S(7.8), S(9)])) 49 | self.assertEqual("0J1 ¯1J0 0J¯1 1", APLArray([4], [S(1j), S(-1), S(-1j), S(1)])) 50 | 51 | def test_nested_vectors(self): 52 | self.assertEqual( 53 | "(0 1) (2 3) (4 5)", 54 | APLArray([3], [APLArray([2], [S(0), S(1)]), APLArray([2], [S(2), S(3)]), APLArray([2], [S(4), S(5)])]) 55 | ) 56 | self.assertEqual( 57 | "1 2 3 (4 5) (6 7)", 58 | APLArray([5], [S(1), S(2), S(3), APLArray([2], [S(4), S(5)]), APLArray([2], [S(6), S(7)])]) 59 | ) 60 | self.assertEqual( 61 | "1 (2 (3 (4 5)))", 62 | APLArray([2], [S(1), APLArray([2], [S(2), APLArray([2], [S(3), APLArray([2], [S(4), S(5)])])])]) 63 | ) 64 | self.assertEqual( 65 | "(((1 2) 3) 4) 5", 66 | APLArray([2], [APLArray([2], [APLArray([2], [APLArray([2], [S(1), S(2)]), S(3)]), S(4)]), S(5)]) 67 | ) 68 | -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic tests for the APL primitive functions. 3 | 4 | The tests presented here only use a single primitive per test. 5 | """ 6 | 7 | from arraymodel import APLArray 8 | from utils import APLTestCase, run, S 9 | 10 | class TestPlus(APLTestCase): 11 | """Test the primitive function +.""" 12 | 13 | def test_conjugate(self): 14 | # With scalars. 15 | self.assertEqual("+0", "0") 16 | self.assertEqual("+¯4.4", "¯4.4") 17 | self.assertEqual("+0J1", "0J¯1") 18 | self.assertEqual("+2.3J¯3.5", "2.3J3.5") 19 | # With a vector argument. 20 | self.assertEqual( 21 | "+ 0 ¯4.4 0J1 2.3J¯3.5", 22 | "0 ¯4.4 0J¯1 2.3J3.5" 23 | ) 24 | # With a nested argument. 25 | self.assertEqual( 26 | "+ 0J1 3J¯1 (3 1J¯0.33) (1J1 0 (0J¯0 0.1J0.1))", 27 | "0J¯1 3J1 (3 1J0.33) (1J¯1 0 (0 0.1J¯0.1))" 28 | ) 29 | 30 | def test_addition(self): 31 | # With scalar arguments. 32 | self.assertEqual("0 + 0", "0") 33 | self.assertEqual("3 + 5", "8") 34 | self.assertEqual("¯0.3 + 0.3", "0") 35 | self.assertEqual("0J1 + 1", "1J1") 36 | self.assertEqual("¯3J0.5 + 3J12", "0J12.5") 37 | # With vector arguments. 38 | self.assertEqual( 39 | "0 1 2 + 3 4 5", 40 | "3 5 7" 41 | ) 42 | self.assertEqual( 43 | "0 3 ¯0.3 0J1 ¯3J0.5 + 0 5 0.3 1 3J12", 44 | "0 8 0 1J1 0J12.5" 45 | ) 46 | # Testing uneven pervasion. 47 | self.assertEqual( 48 | "10 + 0 1 2 3 4 5", 49 | "10 11 12 13 14 15" 50 | ) 51 | self.assertEqual( 52 | "¯1 2 ¯3 4 ¯5 + 3", 53 | "2 5 0 7 ¯2" 54 | ) 55 | self.assertEqual( 56 | "0 (1 2) 0.1 + (1 2 3 4) (0.7 (1J1 2J3)) (5 (7 (9 10J1)))", 57 | "(1 2 3 4) (1.7 (3J1 4J3)) (5.1 (7.1 (9.1 10.1J1)))" 58 | ) 59 | 60 | class TestMinus(APLTestCase): 61 | """Test the primitive function -.""" 62 | 63 | def test_symmetric(self): 64 | # With scalars. 65 | self.assertEqual("-0", "0") 66 | self.assertEqual("-¯4.4", "4.4") 67 | self.assertEqual("-0J1", "0J¯1") 68 | self.assertEqual("-2.3J¯3.5", "¯2.3J3.5") 69 | # With a vector argument. 70 | self.assertEqual( 71 | "- 0 ¯4.4 0J1 2.3J¯3.5", 72 | "0 4.4 0J¯1 ¯2.3J3.5" 73 | ) 74 | # With a nested argument. 75 | self.assertEqual( 76 | "- 0J1 3J¯1 (3 1J¯0.33) (1J1 0 (0J¯0 0.1J0.1))", 77 | "0J¯1 ¯3J1 (¯3 ¯1J0.33) (¯1J¯1 0 (0 ¯0.1J¯0.1))" 78 | ) 79 | 80 | def test_subtraction(self): 81 | # With scalar arguments. 82 | self.assertEqual("0 - 0", "0") 83 | self.assertEqual("3 - 5", "¯2") 84 | self.assertEqual("¯0.3 - 0.3", "¯0.6") 85 | self.assertEqual("0J1 - 1", "¯1J1") 86 | self.assertEqual("¯3J0.5 - 3J12", "¯6J¯11.5") 87 | # With vector arguments. 88 | self.assertEqual( 89 | "0 1 2 - 3 4 5", 90 | "¯3 ¯3 ¯3" 91 | ) 92 | self.assertEqual( 93 | "0 3 ¯0.3 0J1 ¯3J0.5 - 0 5 0.3 1 3J12", 94 | "0 ¯2 ¯0.6 ¯1J1 ¯6J¯11.5" 95 | ) 96 | # Testing uneven pervasion. 97 | self.assertEqual( 98 | "10 - 0 1 2 3 4 5", 99 | "10 9 8 7 6 5" 100 | ) 101 | self.assertEqual( 102 | "¯1 2 ¯3 4 ¯5 - 3", 103 | "¯4 ¯1 ¯6 1 ¯8" 104 | ) 105 | self.assertEqual( 106 | "0 (1 2) 0.1 - (1 2 3 4) (0.5 (1J1 2J3)) (5 (7 (9 10J1)))", 107 | "(¯1 ¯2 ¯3 ¯4) (0.5 (1J¯1 0J¯3)) (¯4.9 (¯6.9 (¯8.9 ¯9.9J¯1)))" 108 | ) 109 | 110 | class TestTimes(APLTestCase): 111 | """Test the primitive function ×.""" 112 | 113 | def test_sign(self): 114 | self.assertEqual("×0", "0") 115 | self.assertEqual("ׯ1", "¯1") 116 | self.assertEqual("ׯ3", "¯1") 117 | self.assertEqual("×1", "1") 118 | self.assertEqual("×51.6", "1") 119 | # Complex numbers. 120 | self.assertEqual("×0J1", "0J1") 121 | self.assertEqual("×0J¯1", "0J¯1") 122 | self.assertEqual("×3J¯4", "0.6J¯0.8") 123 | self.assertEqual("ׯ7J24", "¯0.28J0.96") 124 | # With a vector argument. 125 | self.assertEqual( 126 | "×0 ¯1 ¯3 1 51.6 0J1 0J¯1 3J¯4 ¯7J24", 127 | "0 ¯1 ¯1 1 1 0J1 0J¯1 0.6J¯0.8 ¯0.28J0.96" 128 | ) 129 | # With a nested argument. 130 | self.assertEqual( 131 | "×0 (¯1 ¯3 1) (51.6 0J1 (0J¯1 3J¯4 ¯7J24))", 132 | "0 (¯1 ¯1 1) (1 0J1 (0J¯1 0.6J¯0.8 ¯0.28J0.96))" 133 | ) 134 | 135 | def test_multiplication(self): 136 | self.assertEqual("0 × 0", "0") 137 | self.assertEqual("1 × 0", "0") 138 | self.assertEqual("0 × 1", "0") 139 | self.assertEqual("1 × 0J¯3.3", "0J¯3.3") 140 | self.assertEqual("13 × 12", "156") 141 | # Complex numbers. 142 | self.assertEqual("0J1 × 0J1", "¯1") 143 | self.assertEqual("0J1 × 0J¯1", "1") 144 | self.assertEqual("3J4 × 3J4", "¯7J24") 145 | self.assertEqual("0J0 × 45.3J¯42", "0") 146 | # Vector arguments. 147 | self.assertEqual( 148 | "0 1 0 1 13 0J1 0J1 3J4 0J0 × 0 0 1 0J¯3.3 12 0J1 0J¯1 3J4 45.3J¯42", 149 | "0 0 0 0J¯3.3 156 ¯1 1 ¯7J24 0" 150 | ) 151 | self.assertEqual( 152 | "1 × 0 0 1 0J¯3.3 12 0J1 0J¯1 3J4 45.3J¯42", 153 | "0 0 1 0J¯3.3 12 0J1 0J¯1 3J4 45.3J¯42" 154 | ) 155 | self.assertEqual( 156 | "0 0 1 0J¯3.3 12 0J1 0J¯1 3J4 45.3J¯42 × 2", 157 | "0 0 2 0J¯6.6 24 0J2 0J¯2 6J8 90.6J¯84" 158 | ) 159 | # Test nesting. 160 | self.assertEqual( 161 | "0 (1 0) (1 13 (0J1 0J1) 3J4) 0J0 × 0 (0 1) (0J¯3.3 12 (0J1 0J¯1) 3J4) 45.3J¯42", 162 | "0 (0 0) (0J¯3.3 156 (¯1 1) ¯7J24) 0" 163 | ) 164 | 165 | class TestDivide(APLTestCase): 166 | """Test the primitive function ÷.""" 167 | 168 | def test_inverse(self): 169 | self.assertEqual("÷1", "1") 170 | self.assertEqual("÷2", "0.5") 171 | self.assertEqual("÷4", "0.25") 172 | self.assertEqual("÷¯8", "¯0.125") 173 | # Complex numbers. 174 | self.assertEqual("÷0J1", "0J¯1") 175 | self.assertEqual("÷1J1", "0.5J¯0.5") 176 | self.assertEqual("÷¯1J1", "¯0.5J¯0.5") 177 | self.assertEqual("÷¯1J¯1", "¯0.5J0.5") 178 | self.assertEqual("÷1J¯1", "0.5J0.5") 179 | # With a vector argument. 180 | self.assertEqual( 181 | "÷1 2 4 ¯8 0J1 1J1 ¯1J1 ¯1J¯1 1J¯1", 182 | "1 0.5 0.25 ¯0.125 0J¯1 0.5J¯0.5 ¯0.5J¯0.5 ¯0.5J0.5 0.5J0.5" 183 | ) 184 | # With a nested argument. 185 | self.assertEqual( 186 | "÷(1 2 (4 ¯8 0J1) (1J1 ¯1J1 ¯1J¯1)) 1J¯1", 187 | "(1 0.5 (0.25 ¯0.125 0J¯1) (0.5J¯0.5 ¯0.5J¯0.5 ¯0.5J0.5)) 0.5J0.5" 188 | ) 189 | 190 | def test_division(self): 191 | self.assertEqual("1 ÷ 1", "1") 192 | self.assertEqual("¯46.5 ÷ 1", "¯46.5") 193 | self.assertEqual("3J¯4 ÷ 0J2", "¯2J¯1.5") 194 | self.assertEqual("¯7J24 ÷ 3J4", "3J4") 195 | self.assertEqual("156 ÷ 13", "12") 196 | # With vector arguments. 197 | self.assertEqual( 198 | "1 ¯46.5 3J¯4 ¯7J24 156 ÷ 0J1", 199 | "0J¯1 0J46.5 ¯4J¯3 24J7 0J¯156" 200 | ) 201 | self.assertEqual( 202 | "12 ÷ 1 2 3 4 6 8 12 24", 203 | "12 6 4 3 2 1.5 1 0.5" 204 | ) 205 | self.assertEqual( 206 | "1 ¯46.5 3J¯4 ¯7J24 156 1 12 ÷ 1 1 0J2 3J4 13 0J1 24", 207 | "1 ¯46.5 ¯2J¯1.5 3J4 12 0J¯1 0.5" 208 | ) 209 | # With a nested argument. 210 | self.assertEqual( 211 | "12 (1 3J¯4 ¯7J24) ÷ (1 2 3 4) 0J1", 212 | "(12 6 4 3) (0J¯1 ¯4J¯3 24J7)" 213 | ) 214 | 215 | class TestCeiling(APLTestCase): 216 | """Test the primitive function ⌈.""" 217 | 218 | def test_ceiling(self): 219 | self.assertEqual("⌈0", "0") 220 | self.assertEqual("⌈1", "1") 221 | self.assertEqual("⌈¯1", "¯1") 222 | self.assertEqual("⌈0.1", "1") 223 | self.assertEqual("⌈0.999", "1") 224 | self.assertEqual("⌈¯1.5", "¯1") 225 | 226 | def test_max(self): 227 | self.assertEqual("0 ⌈ 1", "1") 228 | self.assertEqual("1.5 ⌈ 3.4", "3.4") 229 | self.assertEqual("¯1 ⌈ ¯1.1", "¯1") 230 | self.assertEqual("0 ⌈ 0", "0") 231 | self.assertEqual("0.333 ⌈ 0.334", "0.334") 232 | 233 | class TestFloor(APLTestCase): 234 | """Test the primitive function ⌊.""" 235 | 236 | def test_floor(self): 237 | self.assertEqual("⌊0", "0") 238 | self.assertEqual("⌊1", "1") 239 | self.assertEqual("⌊¯1", "¯1") 240 | self.assertEqual("⌊0.1", "0") 241 | self.assertEqual("⌊0.999", "0") 242 | self.assertEqual("⌊¯1.5", "¯2") 243 | 244 | def test_min(self): 245 | self.assertEqual("0 ⌊ 1", "0") 246 | self.assertEqual("1.5 ⌊ 3.4", "1.5") 247 | self.assertEqual("¯1 ⌊ ¯1.1", "¯1.1") 248 | self.assertEqual("0 ⌊ 0", "0") 249 | self.assertEqual("0.333 ⌊ 0.334", "0.333") 250 | 251 | class TestTacks(APLTestCase): 252 | """Test the primitive functions ⊢ and ⊣.""" 253 | 254 | def test(self): 255 | strings = [ 256 | "1", 257 | "1 2 3", 258 | "1 (2 3) 4.5 0J3", 259 | "4.5", 260 | "¯1", 261 | "0J1", 262 | ] 263 | for right in strings: 264 | # Test monadic version. 265 | with self.subTest(right=right): 266 | self.assertEqual(f"⊢{right}", right) 267 | self.assertEqual(f"⊣{right}", right) 268 | 269 | # Test dyadic version. 270 | for left in strings: 271 | with self.subTest(left=left, right=right): 272 | self.assertEqual(f"{left} ⊢ {right}", right) 273 | self.assertEqual(f"{left} ⊣ {right}", left) 274 | 275 | class TestLess(APLTestCase): 276 | """Test the primitive function <.""" 277 | 278 | def test(self): 279 | self.assertEqual("5 < ¯3 0 1 5 10", "0 0 0 0 1") 280 | self.assertEqual("¯2.3 < ¯3 0 1 5 10", "0 1 1 1 1") 281 | 282 | class TestLessEq(APLTestCase): 283 | """Test the primitive function ≤.""" 284 | 285 | def test(self): 286 | self.assertEqual("5 ≤ ¯3 0 1 5 10", "0 0 0 1 1") 287 | self.assertEqual("¯2.3 ≤ ¯3 0 1 5 10", "0 1 1 1 1") 288 | 289 | class TestEq(APLTestCase): 290 | """Test the primitive function =.""" 291 | 292 | def test(self): 293 | self.assertEqual( 294 | "5 ¯2.3 3.5J¯2 = (5 ¯2.3 3.5J¯2) (5 ¯2.3 3.5J¯2) (5 ¯2.3 3.5J¯2)", 295 | "(1 0 0) (0 1 0) (0 0 1)" 296 | ) 297 | 298 | class TestGreaterEq(APLTestCase): 299 | """Test the primitive function ≥.""" 300 | 301 | def test(self): 302 | self.assertEqual("5 ≥ ¯3 0 1 5 10", "1 1 1 1 0") 303 | self.assertEqual("¯2.3 ≥ ¯3 0 1 5 10", "1 0 0 0 0") 304 | 305 | class TestGreater(APLTestCase): 306 | """Test the primitive function >.""" 307 | 308 | def test(self): 309 | self.assertEqual("5 > ¯3 0 1 5 10", "1 1 1 0 0") 310 | self.assertEqual("¯2.3 > ¯3 0 1 5 10", "1 0 0 0 0") 311 | 312 | class TestNeq(APLTestCase): 313 | """Test the primitive function ≠.""" 314 | 315 | def test_unique_mask(self): 316 | self.assertEqual("≠1 2 3 4", "1 1 1 1") 317 | self.assertEqual("≠1 1 1 1", "1 0 0 0") 318 | self.assertEqual("≠1 2 3 1 2 4", "1 1 1 0 0 1") 319 | self.assertEqual("≠(0 0) (1 0) (0 1) 1 0", "1 1 1 1 1") 320 | self.assertEqual("≠1 (1 1) (1 1 1) (1 1 1 1) (1 1)", "1 1 1 1 0") 321 | self.assertEqual("≠0J1 0J¯1", "1 1") 322 | 323 | def test_neq(self): 324 | self.assertEqual( 325 | "5 ¯2.3 3.5J¯2 ≠ (5 ¯2.3 3.5J¯2) (5 ¯2.3 3.5J¯2) (5 ¯2.3 3.5J¯2)", 326 | "(0 1 1) (1 0 1) (1 1 0)" 327 | ) 328 | 329 | class TestLShoe(APLTestCase): 330 | """Test the primitive function ⊂.""" 331 | 332 | def test_enclose(self): 333 | self.assertEqual("⊂1", "1") 334 | self.assertEqual("⊂⊂3J1", "3J1") 335 | self.assertEqual("⊂⊂⊂⊂⊂¯3.4", "¯3.4") 336 | # Now when enclosing actually matters. 337 | arr_str = "1 3J1 (5 8)" 338 | arr = run(arr_str) 339 | for _ in range(5): 340 | arr = S(arr) 341 | arr_str = "⊂" + arr_str 342 | self.assertEqual(arr_str, arr) 343 | 344 | def test_partitioned_enclose(self): 345 | pass 346 | 347 | class TestWithout(APLTestCase): 348 | """Test the primitive function ~.""" 349 | 350 | def test_not(self): 351 | self.assertEqual("~1 0 0 1", "0 1 1 0") 352 | self.assertEqual("~1.0 0.0 1J0 0.0J0", "0 1 0 1") 353 | 354 | def test_without(self): 355 | self.assertEqual("1 2 3 4.5 ~ 2 4.5", "1 3") 356 | self.assertEqual("1 2 3 1 2 3 0J1 0J1 1.0 4.5 ~ 1 2 3", "0J1 0J1 4.5") 357 | 358 | class TestIota(APLTestCase): 359 | """Test the primitive function ⍳.""" 360 | 361 | def test_index_generator(self): 362 | self.assertEqual("⍳2", "0 1") 363 | self.assertEqual("⍳6", "0 1 2 3 4 5") 364 | 365 | def test_index_of(self): 366 | pass 367 | 368 | class TestRho(APLTestCase): 369 | """Test the primitive function ⍴.""" 370 | 371 | def test_shape(self): 372 | self.assertEqual("⍴1 2 3 4", APLArray([1], [S(4)])) 373 | self.assertEqual("⍴0J1", APLArray([0], [])) 374 | self.assertEqual("⍴(0 1 3) (4 5 6) (1 (3 ¯3))", APLArray([1], [S(3)])) 375 | 376 | def test_reshape(self): 377 | self.assertEqual("0⍴1 2 3", APLArray([0], [])) 378 | self.assertEqual("1⍴1", APLArray([1], [S(1)])) 379 | self.assertEqual( 380 | "1⍴(1 2 3)(4 5 6)", 381 | APLArray([1], [APLArray([3], [S(1), S(2), S(3)])]) 382 | ) 383 | self.assertEqual("5⍴1 2 3", "1 2 3 1 2") 384 | self.assertEqual("1 1⍴0J1", APLArray([1,1], [S(1j)])) 385 | 386 | def test_shape_of_reshape(self): 387 | datas = [ 388 | "0", 389 | "0J¯1", 390 | "1 ¯2 3.5 0J4", 391 | "(1 2 3) 4 (5 (6 7 8))", 392 | ] 393 | shapes = [ 394 | "3 4", 395 | "4 3", 396 | "1 1 1", 397 | "2 3 4", 398 | "0 0 0", 399 | "0 0 1", 400 | "0 1 0", 401 | "0 1 1", 402 | "1 0 0", 403 | "1 0 1", 404 | "1 1 1", 405 | ] 406 | for data in datas: 407 | for shape in shapes: 408 | with self.subTest(data=data, shape=shape): 409 | self.assertEqual(f"⍴{shape}⍴{data}", shape) 410 | 411 | class TestAnd(APLTestCase): 412 | """Test the primitive function ∧.""" 413 | 414 | def test_and(self): 415 | self.assertEqual("0 ∧ 0", "0") 416 | self.assertEqual("0 ∧ 1", "0") 417 | self.assertEqual("1 ∧ 0", "0") 418 | self.assertEqual("1 ∧ 1", "1") 419 | self.assertEqual("0 ∧ 0 1", "0 0") 420 | self.assertEqual("1 ∧ 0 1", "0 1") 421 | self.assertEqual("0 1 ∧ 0", "0 0") 422 | self.assertEqual("0 1 ∧ 1", "0 1") 423 | self.assertEqual("0 0 1 1 ∧ 0 1 0 1", "0 0 0 1") 424 | 425 | def test_lcm(self): 426 | pass 427 | 428 | class TestOr(APLTestCase): 429 | """Test the primitive function ∨.""" 430 | 431 | def test_or(self): 432 | self.assertEqual("0 ∨ 0", "0") 433 | self.assertEqual("0 ∨ 1", "1") 434 | self.assertEqual("1 ∨ 0", "1") 435 | self.assertEqual("1 ∨ 1", "1") 436 | self.assertEqual("0 ∨ 0 1", "0 1") 437 | self.assertEqual("1 ∨ 0 1", "1 1") 438 | self.assertEqual("0 1 ∨ 0", "0 1") 439 | self.assertEqual("0 1 ∨ 1", "1 1") 440 | self.assertEqual("0 0 1 1 ∨ 0 1 0 1", "0 1 1 1") 441 | 442 | def test_gcd(self): 443 | pass 444 | 445 | class TestNotAnd(APLTestCase): 446 | """Test the primitive function ⍲.""" 447 | 448 | def test_not_and(self): 449 | self.assertEqual("0 ⍲ 0", "1") 450 | self.assertEqual("0 ⍲ 1", "1") 451 | self.assertEqual("1 ⍲ 0", "1") 452 | self.assertEqual("1 ⍲ 1", "0") 453 | self.assertEqual("0 ⍲ 0 1", "1 1") 454 | self.assertEqual("1 ⍲ 0 1", "1 0") 455 | self.assertEqual("0 1 ⍲ 0", "1 1") 456 | self.assertEqual("0 1 ⍲ 1", "1 0") 457 | self.assertEqual("0 0 1 1 ⍲ 0 1 0 1", "1 1 1 0") 458 | 459 | class TestNotOr(APLTestCase): 460 | """Test the primitive function ⍱.""" 461 | 462 | def test_not_or(self): 463 | self.assertEqual("0 ⍱ 0", "1") 464 | self.assertEqual("0 ⍱ 1", "0") 465 | self.assertEqual("1 ⍱ 0", "0") 466 | self.assertEqual("1 ⍱ 1", "0") 467 | self.assertEqual("0 ⍱ 0 1", "1 0") 468 | self.assertEqual("1 ⍱ 0 1", "0 0") 469 | self.assertEqual("0 1 ⍱ 0", "1 0") 470 | self.assertEqual("0 1 ⍱ 1", "0 0") 471 | self.assertEqual("0 0 1 1 ⍱ 0 1 0 1", "1 0 0 0") 472 | 473 | class TestDecode(APLTestCase): 474 | """Test the primitive function ⊥.""" 475 | 476 | def test_decode(self): 477 | self.assertEqual("0 ⊥ 3", "3") 478 | self.assertEqual("10000 ⊥ 4", "4") 479 | self.assertEqual("42 ⊥ 5", "5") 480 | self.assertEqual("5 ⊥ 6", "6") 481 | self.assertEqual("¯10 ⊥ 7", "7") 482 | self.assertEqual("2 ⊥ 1 0 1", "5") 483 | self.assertEqual("2 2 2 ⊥ 1 0 1", "5") 484 | self.assertEqual("2 3 2 ⊥ 1 0 1", "7") 485 | self.assertEqual("5 3 2 ⊥ 1 0 1", "7") 486 | self.assertEqual("1 0 1 ⊥ 2", "4") 487 | 488 | def test_high_rank(self): 489 | self.assertEqual( 490 | "(3 2⍴8 8 0 8 6 3) ⊥ 2 4⍴2 9 3 1 0 3 8 0", 491 | "3 4⍴16 75 32 8 16 75 32 8 6 30 17 3" 492 | ) 493 | self.assertEqual( 494 | "(3 4 2⍴3 5 6 9 9 1 4 7 7 4 10 8 1 4 9 9 1 6 5 8 6 7 8 6) ⊥ 2 5 4⍴9 7 1 3 10 6 2 3 4 10 8 4 1 9 3 1 3 3 10 8 3 1 9 5 10 3 3 9 9 1 2 10 10 8 6 8 7 6 9 8", 495 | "3 4 5 4⍴48 36 14 20 60 33 13 24 29 51 42 30 15 53 21 13 22 21 59 48 84 64 18 32 100 57 21 36 45 91 74 46 19 89 33 17 34 33 99 80 12 8 10 8 20 9 5 12 13 11 10 14 11 17 9 9 10 9 19 16 66 50 16 26 80 45 17 30 37 71 58 38 17 71 27 15 28 27 79 64 39 29 13 17 50 27 11 21 25 41 34 26 14 44 18 12 19 18 49 40 75 57 17 29 90 51 19 33 41 81 66 42 18 80 30 16 31 30 89 72 39 29 13 17 50 27 11 21 25 41 34 26 14 44 18 12 19 18 49 40 84 64 18 32 100 57 21 36 45 91 74 46 19 89 33 17 34 33 99 80 57 43 15 23 70 39 15 27 33 61 50 34 16 62 24 14 25 24 69 56 75 57 17 29 90 51 19 33 41 81 66 42 18 80 30 16 31 30 89 72 66 50 16 26 80 45 17 30 37 71 58 38 17 71 27 15 28 27 79 64 57 43 15 23 70 39 15 27 33 61 50 34 16 62 24 14 25 24 69 56" 496 | ) 497 | self.assertEqual( 498 | "(3 4 2⍴7 8 9 3 1 9 0 0 2 3 4 9 1 8 9 0 4 7 4 8 8 5 4 1) ⊥ 2 5 4⍴2 9 4 0 2 5 5 7 2 5 0 9 5 2 9 4 1 0 4 3 1 8 0 7 2 6 8 4 4 6 6 7 0 2 2 2 1 4 7 0", 499 | "3 4 5 4⍴17 80 32 7 18 46 48 60 20 46 6 79 40 18 74 34 9 4 39 24 7 35 12 7 8 21 23 25 10 21 6 34 15 8 29 14 4 4 19 9 19 89 36 7 20 51 53 67 22 51 6 88 45 20 83 38 10 4 43 27 1 8 0 7 2 6 8 4 4 6 6 7 0 2 2 2 1 4 7 0 7 35 12 7 8 21 23 25 10 21 6 34 15 8 29 14 4 4 19 9 19 89 36 7 20 51 53 67 22 51 6 88 45 20 83 38 10 4 43 27 17 80 32 7 18 46 48 60 20 46 6 79 40 18 74 34 9 4 39 24 1 8 0 7 2 6 8 4 4 6 6 7 0 2 2 2 1 4 7 0 15 71 28 7 16 41 43 53 18 41 6 70 35 16 65 30 8 4 35 21 17 80 32 7 18 46 48 60 20 46 6 79 40 18 74 34 9 4 39 24 11 53 20 7 12 31 33 39 14 31 6 52 25 12 47 22 6 4 27 15 3 17 4 7 4 11 13 11 6 11 6 16 5 4 11 6 2 4 11 3" 500 | ) 501 | 502 | class TestEncode(APLTestCase): 503 | """Test the primitive function ⊤.""" 504 | 505 | def test_encode(self): 506 | self.assertEqual("4 ⊤ 2", "2") 507 | self.assertEqual("4 ⊤ 4", "0") 508 | self.assertEqual("4 ⊤ 6", "2") 509 | self.assertEqual("2 2 2 2 ⊤ 5", "0 1 0 1") 510 | self.assertEqual("2 2 2 2 ⊤ 5 7 12", "4 3⍴0 0 1 1 1 1 0 1 0 1 1 0") 511 | 512 | def test_high_rank(self): 513 | self.assertEqual( 514 | "(2 2⍴1 2 3 4) ⊤ 3 3⍴1 2 3 4 5 6 7 8 9", 515 | "2 2 3 3⍴0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 1 2 0 1 2 0 1 2 0 1 2 3 0 1 2 3 0 1" 516 | ) 517 | self.assertEqual( 518 | "(2 3⍴3 0 0 6 8 2) ⊤ 4 2⍴6 5 3 5 0 6 3 5", 519 | "2 3 4 2⍴1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 3 2 1 2 0 3 1 2 0 5 3 5 0 0 3 5 6 5 3 5 0 6 3 5 0 1 1 1 0 0 1 1" 520 | ) 521 | -------------------------------------------------------------------------------- /tests/test_tokenizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the RGSPL Tokenizer. 3 | """ 4 | 5 | import unittest 6 | 7 | from rgspl import Parser, Tokenizer, Token 8 | from arraymodel import APLArray 9 | 10 | class TestTokenizer(unittest.TestCase): 11 | """Test the tokenizer.""" 12 | 13 | tok = lambda _, s: Tokenizer(s).tokenize() 14 | eof = Token(Token.EOF, None) 15 | 16 | def test_integers(self): 17 | """Test integer tokenization.""" 18 | 19 | for n in range(30): 20 | self.assertEqual(self.tok(str(n)), [self.eof, Token(Token.INTEGER, n)]) 21 | self.assertEqual(self.tok(f"¯{n}"), [self.eof, Token(Token.INTEGER, -n)]) 22 | 23 | def test_floats(self): 24 | """Test float tokenization.""" 25 | 26 | self.assertEqual(self.tok("0.5"), [self.eof, Token(Token.FLOAT, 0.5)]) 27 | self.assertEqual(self.tok("¯0.25"), [self.eof, Token(Token.FLOAT, -0.25)]) 28 | self.assertEqual(self.tok(".125"), [self.eof, Token(Token.FLOAT, 0.125)]) 29 | self.assertEqual(self.tok("¯.0625"), [self.eof, Token(Token.FLOAT, -0.0625)]) 30 | 31 | def test_complex_nums(self): 32 | """Test complex number tokenization.""" 33 | 34 | T = Token 35 | C = lambda c: [self.eof, T(T.COMPLEX, c)] 36 | realss = ["0", "2", "¯3", "0.5", "¯0.25", ".125", "¯.5"] 37 | realsv = [0, 2, -3, 0.5, -0.25, 0.125, -0.5] 38 | compss = ["1", "¯4", "0.1", "¯0.01", ".2", "¯.8"] 39 | compsv = [1, -4, 0.1, -0.01, 0.2, -0.8] 40 | for rs, rv in zip(realss, realsv): 41 | for cs, cv in zip(compss, compsv): 42 | c = complex(rv, cv) 43 | with self.subTest(c=c): 44 | self.assertEqual(self.tok(f"{rs}J{cs}"), C(c)) 45 | 46 | def test_numeric_promotion(self): 47 | """Ensure empty imaginary parts and empty decimals get promoted.""" 48 | 49 | f = lambda t: lambda v: [self.eof, Token(t, v)] 50 | I = f(Token.INTEGER) 51 | F = f(Token.FLOAT) 52 | 53 | self.assertEqual(self.tok("1."), I(1)) 54 | self.assertEqual(self.tok("56.0"), I(56)) 55 | self.assertEqual(self.tok("¯987."), I(-987)) 56 | self.assertEqual(self.tok("¯23.0"), I(-23)) 57 | 58 | self.assertEqual(self.tok("1J0"), I(1)) 59 | self.assertEqual(self.tok("2J¯0"), I(2)) 60 | self.assertEqual(self.tok("1.2J0"), F(1.2)) 61 | self.assertEqual(self.tok("¯0.5J0"), F(-0.5)) 62 | 63 | self.assertEqual(self.tok("1.0J0."), I(1)) 64 | self.assertEqual(self.tok("¯8.J0.0"), I(-8)) 65 | self.assertEqual(self.tok(".125J0.0"), F(0.125)) 66 | self.assertEqual(self.tok("¯.25J0."), F(-0.25)) 67 | 68 | def test_ids(self): 69 | """Test id tokenization.""" 70 | 71 | ID = lambda v: [self.eof, Token(Token.ID, v)] 72 | self.assertEqual(self.tok("bananas"), ID("bananas")) 73 | self.assertEqual(self.tok("abcd"), ID("abcd")) 74 | self.assertEqual(self.tok("CamelCase"), ID("CamelCase")) 75 | self.assertEqual(self.tok("pascalCase"), ID("pascalCase")) 76 | self.assertEqual(self.tok("using_Some_Underscores"), ID("using_Some_Underscores")) 77 | self.assertEqual(self.tok("_"), ID("_")) 78 | self.assertEqual(self.tok("__"), ID("__")) 79 | self.assertEqual(self.tok("_varname"), ID("_varname")) 80 | self.assertEqual(self.tok("var123"), ID("var123")) 81 | self.assertEqual(self.tok("var123_2"), ID("var123_2")) 82 | self.assertEqual(self.tok("_J3"), ID("_J3")) 83 | self.assertEqual(self.tok("_1J2"), ID("_1J2")) 84 | self.assertEqual(self.tok("J2"), ID("J2")) 85 | 86 | def test_comment_skipping(self): 87 | """Test if comments are skipped.""" 88 | 89 | self.assertEqual(self.tok("⍝ this is a comment"), [self.eof]) 90 | self.assertEqual(self.tok("⍝3J5 ¯3"), [self.eof]) 91 | self.assertEqual(self.tok("⍝⍝⍝ triple comment"), [self.eof]) 92 | self.assertEqual(self.tok("¯2 ⍝ neg 2"), [self.eof, Token(Token.INTEGER, -2)]) 93 | self.assertEqual(self.tok("var ⍝ some var"), [self.eof, Token(Token.ID, "var")]) 94 | 95 | def test_wysiwyg_tokens(self): 96 | """Test if WYSIWYG tokens are tokenized correctly.""" 97 | 98 | for s, type_ in Token.WYSIWYG_MAPPING.items(): 99 | for s2, type_2 in Token.WYSIWYG_MAPPING.items(): 100 | code = s+s2 101 | toks = [self.eof, Token(type_, s), Token(type_2, s2)] 102 | with self.subTest(code=code): 103 | self.assertEqual(self.tok(code), toks) 104 | code = s+" "+s2 105 | with self.subTest(code=code): 106 | self.assertEqual(self.tok(code), toks) 107 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions used by the tests. 3 | """ 4 | 5 | import functools 6 | import unittest 7 | 8 | from rgspl import Interpreter, Parser, Tokenizer 9 | from arraymodel import APLArray 10 | 11 | def run(code): 12 | """Run a string containing APL code.""" 13 | return Interpreter(Parser(Tokenizer(code))).interpret() 14 | 15 | def S(scalar): 16 | """Create an APL scalar.""" 17 | return APLArray([], [scalar]) 18 | 19 | def run_apl_code_decorator(assert_method): 20 | """Create a new assert method interpreting positional strings as APL code.""" 21 | 22 | @functools.wraps(assert_method) 23 | def new_assert_method(*args, **kwargs): 24 | i = 0 25 | args = list(args) # to allow in-place modification. 26 | # Run, as APL code, the first consecutive strings in the positional arguments. 27 | while i < len(args) and isinstance(args[i], str): 28 | args[i] = run(args[i]) 29 | i += 1 30 | return assert_method(*args, **kwargs) 31 | return new_assert_method 32 | 33 | class APLTestCase(unittest.TestCase): 34 | """The assert methods preprocess the arguments by running the APL code. 35 | 36 | A test case class that overrides some assert methods that start by running 37 | the APL code in the arguments and only then applying the assertions over them. 38 | """ 39 | 40 | def __init__(self, *args, **kwargs): 41 | unittest.TestCase.__init__(self, *args, **kwargs) 42 | 43 | # Traverse all the methods of the unittest.TestCase, looking for assertX 44 | # methods and decorating them accordingly. 45 | for method_name in dir(self): 46 | if method_name.startswith("assert") and not method_name.endswith("_"): 47 | decorated = run_apl_code_decorator(getattr(self, method_name)) 48 | setattr(self, method_name, decorated) 49 | --------------------------------------------------------------------------------