diff --git a/Cargo.lock b/Cargo.lock index 7c1e0531..55f81929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,10 +1172,6 @@ version = "0.3.0" dependencies = [ "ahash", "itertools 0.15.0", - "num", - "ordered-float", - "ryu", - "thiserror", ] [[package]] @@ -1230,7 +1226,6 @@ dependencies = [ "derive_more", "ndc_core", "ndc_lexer", - "num", "thiserror", ] @@ -1263,7 +1258,7 @@ dependencies = [ "ndc_lexer", "ndc_parser", "num", - "ordered-float", + "ryu", "thiserror", ] @@ -1373,15 +1368,6 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" -[[package]] -name = "ordered-float" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" -dependencies = [ - "num-traits", -] - [[package]] name = "page_size" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 5935d5a1..8ca58b87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ ndc_macros = { path = "ndc_macros", version = "0.1.0" } ndc_stdlib = { path = "ndc_stdlib", version = "0.3.0" } num = "0.4.3" once_cell = "1.21.4" -ordered-float = "5.3.0" proptest = "1.11" yansi = { version = "1.0.1", features = ["detect-tty", "detect-env"] } rand = "0.10.2" diff --git a/benches/programs/bigint.ndc b/benches/programs/bigint.ndc index 1fa12ab6..21703a0a 100644 --- a/benches/programs/bigint.ndc +++ b/benches/programs/bigint.ndc @@ -1,4 +1,4 @@ -let result = 1; +let result = 1n; for i in 2..=5000 { result *= i; } diff --git a/benches/programs/closures.ndc b/benches/programs/closures.ndc index a5ddd233..fd9c42c5 100644 --- a/benches/programs/closures.ndc +++ b/benches/programs/closures.ndc @@ -1,6 +1,6 @@ let total = 0; for _ in 0..20 { let adders = [fn(x) => x + i for i in 0..10_000]; - total = total + adders.map(fn(f) => f(0)).sum; + total = total + (adders.map(fn(f) => f(0)) as List).sum; }; print(total); diff --git a/benches/programs/perlin.ndc b/benches/programs/perlin.ndc index d452b581..c0287494 100644 --- a/benches/programs/perlin.ndc +++ b/benches/programs/perlin.ndc @@ -5,7 +5,7 @@ fn fade(t) => t * t * t * (t * (t * 6 - 15) + 10); fn lerp(a, b, t) => a + t * (b - a); -fn gradient(h, x, y) => (vectors[h % 8] * (x, y)).sum +fn gradient(h, x, y) => ((vectors[h % 8] * (x, y)) as Tuple).sum fn perlin(x, y, perm) { let xi, yi = int(x) & 255, int(y) & 255; @@ -33,4 +33,4 @@ fn benchmark_perlin(grid_size) { } let noise = benchmark_perlin(100); -print("Min:", noise.min, "Max:", noise.max, "Avg:", (noise.sum / noise.len)); +print("Min:", noise.min, "Max:", noise.max, "Avg:", ((noise as List).sum / noise.len)); diff --git a/ext/andy-cpp/syntaxes/andy-cpp.tmLanguage.json b/ext/andy-cpp/syntaxes/andy-cpp.tmLanguage.json index f9160242..f5676383 100644 --- a/ext/andy-cpp/syntaxes/andy-cpp.tmLanguage.json +++ b/ext/andy-cpp/syntaxes/andy-cpp.tmLanguage.json @@ -58,7 +58,7 @@ { "comment": "built-in type names used in annotations like `let x: Int` or `-> List`", "name": "support.type.andy-cpp", - "match": "\\b(Any|Never|Bool|Number|Float|Int|Rational|Complex|String|Option|Sequence|List|Iterator|MinHeap|MaxHeap|Deque|Tuple|Map)\\b" + "match": "\\b(Any|Never|Bool|Number|Float|Int|String|Option|Sequence|List|Iterator|MinHeap|MaxHeap|Deque|Tuple|Map)\\b" } ] }, @@ -200,18 +200,18 @@ "name": "constant.numeric.complex.andy-cpp" }, { - "comment": "Floats", - "match": "\\b([0-9][0-9_]*\\.[0-9][0-9_]*)\\b", + "comment": "Primitive Float and advanced Number float literals", + "match": "\\b([0-9][0-9_]*\\.[0-9][0-9_]*n?)\\b", "name": "constant.numeric.float.andy-cpp" }, { - "comment": "Hexadecimal numbers", - "match": "\\b(0x[0-9A-Fa-f][0-9A-Fa-f_]*)\\b", + "comment": "Primitive Int and advanced Number hexadecimal literals", + "match": "\\b(0x[0-9A-Fa-f][0-9A-Fa-f_]*n?)\\b", "name": "constant.numeric.hex.andy-cpp" }, { - "comment": "Ocatal n umbers", - "match": "\\b(0o[0-7][0-7]*)\\b", + "comment": "Primitive Int and advanced Number octal literals", + "match": "\\b(0o[0-7][0-7_]*n?)\\b", "name": "constant.numeric.octal.andy-cpp" }, { @@ -220,13 +220,13 @@ "name": "constant.numeric.andy-cpp" }, { - "comment": "Binary numbers", - "match": "\\b(0b[01][01_]*)\\b", + "comment": "Primitive Int and advanced Number binary literals", + "match": "\\b(0b[01][01_]*n?)\\b", "name": "constant.numeric.binary.andy-cpp" }, { - "comment": "Integer numbers", - "match": "\\b([0-9][0-9_]*)\\b", + "comment": "Primitive Int and advanced Number decimal literals", + "match": "\\b([0-9][0-9_]*n?)\\b", "name": "constant.numeric.decimal.andy-cpp" }, { @@ -492,4 +492,4 @@ } }, "scopeName": "source.andy-cpp" -} \ No newline at end of file +} diff --git a/ext/tree-sitter-andy-cpp/grammar.js b/ext/tree-sitter-andy-cpp/grammar.js index e9bd539c..853c210a 100644 --- a/ext/tree-sitter-andy-cpp/grammar.js +++ b/ext/tree-sitter-andy-cpp/grammar.js @@ -467,14 +467,14 @@ module.exports = grammar({ ), integer: _ => token(choice( - /0b[01][01_]*/, - /0o[0-7][0-7_]*/, - /0x[0-9A-Fa-f][0-9A-Fa-f_]*/, + /0b[01][01_]*n?/, + /0o[0-7][0-7_]*n?/, + /0x[0-9A-Fa-f][0-9A-Fa-f_]*n?/, /[0-9]+r[0-9A-Za-z][0-9A-Za-z_]*/, // arbitrary radix, e.g. 16rFF - /[0-9][0-9_]*/, + /[0-9][0-9_]*n?/, )), - float: _ => token(/[0-9][0-9_]*\.[0-9][0-9_]*/), + float: _ => token(/[0-9][0-9_]*\.[0-9][0-9_]*n?/), // Imaginary part of a complex literal: `3i`, `2.5j` complex: _ => token(/[0-9][0-9_]*(\.[0-9][0-9_]*)?[ij]/), diff --git a/ext/tree-sitter-andy-cpp/src/grammar.json b/ext/tree-sitter-andy-cpp/src/grammar.json index c4bb69c1..a16d09c7 100644 --- a/ext/tree-sitter-andy-cpp/src/grammar.json +++ b/ext/tree-sitter-andy-cpp/src/grammar.json @@ -2705,15 +2705,15 @@ "members": [ { "type": "PATTERN", - "value": "0b[01][01_]*" + "value": "0b[01][01_]*n?" }, { "type": "PATTERN", - "value": "0o[0-7][0-7_]*" + "value": "0o[0-7][0-7_]*n?" }, { "type": "PATTERN", - "value": "0x[0-9A-Fa-f][0-9A-Fa-f_]*" + "value": "0x[0-9A-Fa-f][0-9A-Fa-f_]*n?" }, { "type": "PATTERN", @@ -2721,7 +2721,7 @@ }, { "type": "PATTERN", - "value": "[0-9][0-9_]*" + "value": "[0-9][0-9_]*n?" } ] } @@ -2730,7 +2730,7 @@ "type": "TOKEN", "content": { "type": "PATTERN", - "value": "[0-9][0-9_]*\\.[0-9][0-9_]*" + "value": "[0-9][0-9_]*\\.[0-9][0-9_]*n?" } }, "complex": { diff --git a/ext/tree-sitter-andy-cpp/src/parser.c b/ext/tree-sitter-andy-cpp/src/parser.c index 0c76f4ac..c1ff4d80 100644 --- a/ext/tree-sitter-andy-cpp/src/parser.c +++ b/ext/tree-sitter-andy-cpp/src/parser.c @@ -2138,7 +2138,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { if (eof) ADVANCE(17); ADVANCE_MAP( '!', 52, - '"', 93, + '"', 95, '#', 1, '%', 69, '&', 61, @@ -2150,7 +2150,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { '-', 54, '.', 82, '/', 66, - '0', 84, + '0', 85, ':', 19, ';', 18, '<', 32, @@ -2167,12 +2167,12 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { ); if (('\t' <= lookahead && lookahead <= '\r') || lookahead == ' ') SKIP(0); - if (('1' <= lookahead && lookahead <= '9')) ADVANCE(85); + if (('1' <= lookahead && lookahead <= '9')) ADVANCE(86); if (('A' <= lookahead && lookahead <= '_') || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(101); + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(103); END_STATE(); case 1: - if (lookahead == '!') ADVANCE(102); + if (lookahead == '!') ADVANCE(104); END_STATE(); case 2: ADVANCE_MAP( @@ -2204,7 +2204,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { if (('\t' <= lookahead && lookahead <= '\r') || lookahead == ' ') SKIP(2); if (('A' <= lookahead && lookahead <= '_') || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(101); + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(103); END_STATE(); case 3: ADVANCE_MAP( @@ -2221,16 +2221,16 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { lookahead == ' ') SKIP(3); if (('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(101); + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(103); END_STATE(); case 4: - if (lookahead == '"') ADVANCE(93); - if (lookahead == '#') ADVANCE(95); - if (lookahead == '/') ADVANCE(97); + if (lookahead == '"') ADVANCE(95); + if (lookahead == '#') ADVANCE(97); + if (lookahead == '/') ADVANCE(99); if (lookahead == '\\') ADVANCE(11); if (('\t' <= lookahead && lookahead <= '\r') || - lookahead == ' ') ADVANCE(96); - if (lookahead != 0) ADVANCE(98); + lookahead == ' ') ADVANCE(98); + if (lookahead != 0) ADVANCE(100); END_STATE(); case 5: ADVANCE_MAP( @@ -2252,7 +2252,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { ('a' <= lookahead && lookahead <= 'z')) ADVANCE(30); END_STATE(); case 6: - if (lookahead == '/') ADVANCE(102); + if (lookahead == '/') ADVANCE(104); END_STATE(); case 7: if (lookahead == '=') ADVANCE(72); @@ -2266,7 +2266,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { END_STATE(); case 10: if (lookahead == '0' || - lookahead == '1') ADVANCE(87); + lookahead == '1') ADVANCE(88); END_STATE(); case 11: if (lookahead == '"' || @@ -2274,29 +2274,29 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { lookahead == '\\' || lookahead == 'n' || lookahead == 'r' || - lookahead == 't') ADVANCE(99); + lookahead == 't') ADVANCE(101); END_STATE(); case 12: - if (('0' <= lookahead && lookahead <= '7')) ADVANCE(88); + if (('0' <= lookahead && lookahead <= '7')) ADVANCE(89); END_STATE(); case 13: - if (('0' <= lookahead && lookahead <= '9')) ADVANCE(91); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(93); END_STATE(); case 14: if (('0' <= lookahead && lookahead <= '9') || ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(89); + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(90); END_STATE(); case 15: if (('0' <= lookahead && lookahead <= '9') || ('A' <= lookahead && lookahead <= 'Z') || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(90); + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(91); END_STATE(); case 16: if (eof) ADVANCE(17); ADVANCE_MAP( '!', 52, - '"', 93, + '"', 95, '#', 1, '%', 69, '&', 61, @@ -2308,7 +2308,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { '-', 53, '.', 82, '/', 66, - '0', 84, + '0', 85, ':', 19, ';', 18, '<', 32, @@ -2325,9 +2325,9 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { ); if (('\t' <= lookahead && lookahead <= '\r') || lookahead == ' ') SKIP(16); - if (('1' <= lookahead && lookahead <= '9')) ADVANCE(85); + if (('1' <= lookahead && lookahead <= '9')) ADVANCE(86); if (('A' <= lookahead && lookahead <= '_') || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(101); + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(103); END_STATE(); case 17: ACCEPT_TOKEN(ts_builtin_sym_end); @@ -2507,7 +2507,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { END_STATE(); case 66: ACCEPT_TOKEN(anon_sym_SLASH); - if (lookahead == '/') ADVANCE(102); + if (lookahead == '/') ADVANCE(104); if (lookahead == '=') ADVANCE(40); END_STATE(); case 67: @@ -2575,128 +2575,143 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) { END_STATE(); case 84: ACCEPT_TOKEN(sym_integer); - if (lookahead == '.') ADVANCE(13); - if (lookahead == '_') ADVANCE(86); - if (lookahead == 'b') ADVANCE(10); - if (lookahead == 'o') ADVANCE(12); - if (lookahead == 'r') ADVANCE(15); - if (lookahead == 'x') ADVANCE(14); - if (lookahead == 'i' || - lookahead == 'j') ADVANCE(92); - if (('0' <= lookahead && lookahead <= '9')) ADVANCE(85); END_STATE(); case 85: + ACCEPT_TOKEN(sym_integer); + ADVANCE_MAP( + '.', 13, + '_', 87, + 'b', 10, + 'n', 84, + 'o', 12, + 'r', 15, + 'x', 14, + 'i', 94, + 'j', 94, + ); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(86); + END_STATE(); + case 86: ACCEPT_TOKEN(sym_integer); if (lookahead == '.') ADVANCE(13); - if (lookahead == '_') ADVANCE(86); + if (lookahead == '_') ADVANCE(87); + if (lookahead == 'n') ADVANCE(84); if (lookahead == 'r') ADVANCE(15); if (lookahead == 'i' || - lookahead == 'j') ADVANCE(92); - if (('0' <= lookahead && lookahead <= '9')) ADVANCE(85); + lookahead == 'j') ADVANCE(94); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(86); END_STATE(); - case 86: + case 87: ACCEPT_TOKEN(sym_integer); if (lookahead == '.') ADVANCE(13); + if (lookahead == 'n') ADVANCE(84); if (lookahead == 'i' || - lookahead == 'j') ADVANCE(92); + lookahead == 'j') ADVANCE(94); if (('0' <= lookahead && lookahead <= '9') || - lookahead == '_') ADVANCE(86); + lookahead == '_') ADVANCE(87); END_STATE(); - case 87: + case 88: ACCEPT_TOKEN(sym_integer); + if (lookahead == 'n') ADVANCE(84); if (lookahead == '0' || lookahead == '1' || - lookahead == '_') ADVANCE(87); + lookahead == '_') ADVANCE(88); END_STATE(); - case 88: + case 89: ACCEPT_TOKEN(sym_integer); + if (lookahead == 'n') ADVANCE(84); if (('0' <= lookahead && lookahead <= '7') || - lookahead == '_') ADVANCE(88); + lookahead == '_') ADVANCE(89); END_STATE(); - case 89: + case 90: ACCEPT_TOKEN(sym_integer); + if (lookahead == 'n') ADVANCE(84); if (('0' <= lookahead && lookahead <= '9') || ('A' <= lookahead && lookahead <= 'F') || lookahead == '_' || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(89); + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(90); END_STATE(); - case 90: + case 91: ACCEPT_TOKEN(sym_integer); if (('0' <= lookahead && lookahead <= '9') || ('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(90); + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(91); END_STATE(); - case 91: + case 92: ACCEPT_TOKEN(sym_float); + END_STATE(); + case 93: + ACCEPT_TOKEN(sym_float); + if (lookahead == 'n') ADVANCE(92); if (lookahead == 'i' || - lookahead == 'j') ADVANCE(92); + lookahead == 'j') ADVANCE(94); if (('0' <= lookahead && lookahead <= '9') || - lookahead == '_') ADVANCE(91); + lookahead == '_') ADVANCE(93); END_STATE(); - case 92: + case 94: ACCEPT_TOKEN(sym_complex); END_STATE(); - case 93: + case 95: ACCEPT_TOKEN(anon_sym_DQUOTE); END_STATE(); - case 94: + case 96: ACCEPT_TOKEN(aux_sym_string_token1); - if (lookahead == '\n') ADVANCE(98); + if (lookahead == '\n') ADVANCE(100); if (lookahead != 0 && lookahead != '"' && - lookahead != '\\') ADVANCE(94); + lookahead != '\\') ADVANCE(96); END_STATE(); - case 95: + case 97: ACCEPT_TOKEN(aux_sym_string_token1); - if (lookahead == '!') ADVANCE(94); + if (lookahead == '!') ADVANCE(96); if (lookahead != 0 && lookahead != '!' && lookahead != '"' && - lookahead != '\\') ADVANCE(98); + lookahead != '\\') ADVANCE(100); END_STATE(); - case 96: + case 98: ACCEPT_TOKEN(aux_sym_string_token1); - if (lookahead == '#') ADVANCE(95); - if (lookahead == '/') ADVANCE(97); + if (lookahead == '#') ADVANCE(97); + if (lookahead == '/') ADVANCE(99); if (('\t' <= lookahead && lookahead <= '\r') || - lookahead == ' ') ADVANCE(96); + lookahead == ' ') ADVANCE(98); if (lookahead != 0 && lookahead != '"' && lookahead != '#' && - lookahead != '\\') ADVANCE(98); + lookahead != '\\') ADVANCE(100); END_STATE(); - case 97: + case 99: ACCEPT_TOKEN(aux_sym_string_token1); - if (lookahead == '/') ADVANCE(94); + if (lookahead == '/') ADVANCE(96); if (lookahead != 0 && lookahead != '"' && - lookahead != '\\') ADVANCE(98); + lookahead != '\\') ADVANCE(100); END_STATE(); - case 98: + case 100: ACCEPT_TOKEN(aux_sym_string_token1); if (lookahead != 0 && lookahead != '"' && - lookahead != '\\') ADVANCE(98); + lookahead != '\\') ADVANCE(100); END_STATE(); - case 99: + case 101: ACCEPT_TOKEN(sym_escape_sequence); END_STATE(); - case 100: + case 102: ACCEPT_TOKEN(sym_identifier); END_STATE(); - case 101: + case 103: ACCEPT_TOKEN(sym_identifier); - if (lookahead == '?') ADVANCE(100); + if (lookahead == '?') ADVANCE(102); if (('0' <= lookahead && lookahead <= '9') || ('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(101); + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(103); END_STATE(); - case 102: + case 104: ACCEPT_TOKEN(sym_comment); if (lookahead != 0 && - lookahead != '\n') ADVANCE(102); + lookahead != '\n') ADVANCE(104); END_STATE(); default: return false; diff --git a/ext/tree-sitter-andy-cpp/test/corpus/basics.txt b/ext/tree-sitter-andy-cpp/test/corpus/basics.txt index 22d7b437..98a01bfa 100644 --- a/ext/tree-sitter-andy-cpp/test/corpus/basics.txt +++ b/ext/tree-sitter-andy-cpp/test/corpus/basics.txt @@ -207,3 +207,32 @@ shift right after a cast comparison right: (binary_expression left: (integer) right: (integer)))) + +================== +advanced number literals +================== + +let decimal = 42n; +let float = 1.25n; +let binary = 0b101n; +let octal = 0o77n; +let hexadecimal = 0xffn; + +--- + +(source_file + (variable_declaration + name: (identifier) + value: (integer)) + (variable_declaration + name: (identifier) + value: (float)) + (variable_declaration + name: (identifier) + value: (integer)) + (variable_declaration + name: (identifier) + value: (integer)) + (variable_declaration + name: (identifier) + value: (integer))) diff --git a/manual/src/features/augmented-assignment.md b/manual/src/features/augmented-assignment.md index 3a7b8c76..40f79128 100644 --- a/manual/src/features/augmented-assignment.md +++ b/manual/src/features/augmented-assignment.md @@ -96,7 +96,7 @@ and may widen the inferred type of the target, just like a regular assignment: ```ndc let numbers = [1]; -numbers[0] += 0.5; // fine: the element type widens from Int to Number +numbers[0] += 0.5; // fine: the element type widens from Int to Any assert_eq(numbers, [1.5]); ``` diff --git a/manual/src/reference/types.md b/manual/src/reference/types.md index aa0559a5..1fbced09 100644 --- a/manual/src/reference/types.md +++ b/manual/src/reference/types.md @@ -7,11 +7,9 @@ The type system is hierarchical with `Any` at the root: * `Any` * [`Option`](./types/option.md) * [`Bool`](./types/boolean.md) - * [`Number`](./types/number.md) - * `Int` — machine `i64` or arbitrary-precision `BigInt`, picked automatically - * `Float` - * `Complex` - * `Rational` + * [`Int`](./types/number.md) — checked signed 64-bit integers + * [`Float`](./types/number.md) — IEEE 754 `f64` values + * [`Number`](./types/number.md) — arbitrary-size, rational, floating-point, and complex values * `Sequence` * [`String`](./types/string.md): a mutable list of characters * [`List`](./types/list.md): a mutable list diff --git a/manual/src/reference/types/number.md b/manual/src/reference/types/number.md index 604698d0..4e52884a 100644 --- a/manual/src/reference/types/number.md +++ b/manual/src/reference/types/number.md @@ -1,84 +1,132 @@ # Numbers -Andy C++ has four number types: - - * Int: which is subdivided into `Int64` and `BigInt` to support arbitrarily large numbers - * Float: which is backed by an `f64` - * Rational: which consists of two `BigInt`s and represents a fraction - * Complex: which is a pair of two `f64`'s - -## Operators - -| Operator | Function | Support augmented assignment [[1]](../../features/augmented-assignment.md) | Augmentable with `not` | -| :-: | --- | --- | --- | -| `+` | Addition | `true` | `false` | -| `-` | Subtraction | `true` | `false` | -| unary `-` | Negation | `true` | `false` | -| `*` | Multiplication | `true` | `false` | -| `/` | Division (returns rational for integers) | `true` | `false` | -| `\` | Floor division (integer result, rounds toward negative infinity) | `true` | `false` | -| `^` | Exponentiation | `true` | `false` | -| `%` | C-style modulo (can be negative) | `true` | `false` | -| `%%` | Remainder of [euclidean division](https://en.wikipedia.org/wiki/Euclidean_division) | `true` | `false` | -| `==` | Strict equality | `false` | `true` | -| `<=` | Less or equal | `false` | `true` | -| `<` | Less than | `false` | `true` | -| `>=` | Greater or equal | `false` | `true` | -| `>` | Greater than | `false` | `true` | -| `!=` | Not equal | `false` | `true` | -| `<=>` | Compare | `false` | `false` | -| `>=<` | Reverse compare | `false` | `false` | -| `<>` | Concatenate string values | `true` | `false` | - -### Division by zero - -Dividing by zero with `/` or `\` (floor division) follows floating-point -semantics: the result is promoted to a float and yields infinity (for example -`1 / 0` and `1 \ 0` are both `inf`). The remainder operators `%` (modulo) and -`%%` (euclidean remainder) have no meaningful result for a zero divisor, so they -raise a runtime error (`division by zero`). Floating-point `%` follows IEEE -semantics and returns `NaN`. - -Integers also support these operations: - -| Operator | Function | Support augmented assignment [[1]](../../features/augmented-assignment.md) | Augmentable with `not` | -| :-: | --- | --- | --- | -| `\|` | Bitwise OR | `true` | `false` | -| `&` | Bitwise AND | `true` | `false` | -| `~` | Bitwise XOR, or bitwise NOT in unary position | `true` | `false` | -| unary `~` | bitwise NOT | `true` | `false` | -| `>>` | Bitshift right | `true` | `false` | -| `<<` | Bitshift left | `true` | `false` | - -### Integers - -Andy C++ uses signed 64-bit integers until an expression overflows. At that point it switches to `BigInt` and computes the result with the [num crate](https://crates.io/crates/num). You can compute very large numbers this way, but code such as a naive solution to [Advent of Code 2022 - Day 11](https://adventofcode.com/2022/day/11) may keep allocating until you run out of memory. +Andy C++ exposes three sibling numeric types: + +* `Int` stores a signed 64-bit integer. Checked arithmetic reports overflow. The remainder + operators `%` and `%%` are the exception: once the divisor is non-zero the result always + fits, even where the quotient it implies would not. +* `Float` stores an IEEE 754 `f64`. +* `Number` supports arbitrary-size integers, exact rational values, floats, and complex values. + +`Int`, `Float`, and `Number` share `Any` as their nearest common supertype. An `Int` does not satisfy a `Number` annotation. Use an `n` literal or the `Number` constructor when you need the advanced mode: ```ndc -let result = 2 ^ 1024; +let count: Int = 42; +let measurement: Float = 42.0; +let exact: Number = 42n; + +assert_eq(Number(42), 42n); +assert_eq(Number(42.0), 42.0n); +``` + +## Literals -// Result: 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 +The `n` suffix creates a `Number` from a decimal integer or float. Binary, octal, and hexadecimal integers also accept it: + +```ndc +let large = 123456789123456789123456789n; +let decimal = 1.25n; +let binary = 0b101010n; +let octal = 0o52n; +let hexadecimal = 0x2an; ``` -### Rational numbers and Floats -The math system keeps exact values unless you ask for a float. Dividing integers produces a rational number, not a float. An expression produces a float only when one of its operands is a float. +An integer literal without `n` must fit in `i64`. The lexer reports an error and suggests the suffixed form when it does not fit. -One exception exists: raising an integer to a rational power produces a float. +Arbitrary-radix literals such as `16r2a` remain `Int` literals and do not accept `n`. The `i` and `j` suffixes create complex `Number` values: -For example: ```ndc -let result = 5^(1/2); +let z: Number = 2 + 3i; +assert_eq(z, 2 + 3j); +``` + +## Arithmetic modes + +The arithmetic operators `+`, `-`, `*`, `/`, `\`, `%`, `%%`, and `^` define all nine pairs of `Int`, `Float`, and `Number`. The operands select the result type: -// result is Float because Int^ on Rational results in Float -assert_eq(result, 2.23606797749979); // Using == assertions on floats is risky +| Operands | Result | +| --- | --- | +| `Int`, `Int` | `Int` | +| `Int`, `Float` or `Float`, `Int` | `Float` | +| `Float`, `Float` | `Float` | +| Any pair containing `Number` | `Number` | + +`Int` uses checked `i64` arithmetic. `/` truncates toward zero, while `\` rounds toward negative infinity. `%` pairs with truncating division and `%%` returns a Euclidean remainder: + +```ndc +assert_eq(-7 / 2, -3); +assert_eq(-7 \ 2, -4); +assert_eq(-7 % 2, -1); +assert_eq(-7 %% 2, 1); ``` -### Complex numbers +`Float` follows IEEE 754 behavior. `Number` keeps integer and rational operations exact when it can: + +```ndc +assert_eq(7 / 2, 3); +assert_eq(7n / 2n, 7n / 2n); +assert_eq(2n ^ 100n, 1267650600228229401496703205376n); +``` -Andy C++ supports complex numbers. Use either `i` or `j` as the imaginary unit. Create a complex number by combining a real part with an imaginary part. +Integer powers and shifts must fit their checked `Int` result. Use `Number` for negative exponents, arbitrary-size powers, and complex continuation. Roots, logarithms, inverse trigonometric functions, and fractional powers return a complex `Number` when the real result does not exist: ```ndc -let complex = 5.0 + 3.1j; -let result = complex * 1.3; // 6.5+4.03i -let result = complex + 5.3; // 10.3+3.1i +assert_eq(5n ^ -1n, 1n / 5n); +assert_eq(sqrt(-1n), 1i); ``` + +## Division by zero + +`Int` division and remainder by zero report an error. `Float` returns IEEE infinity or NaN. `Number` also falls back to a wrapped Float result when an exact zero divisor has no rational representation: + +```ndc +assert_eq(1n / 0n, Inf); +assert_eq(1n \ 0n, Inf); + +let nan = 0n / 0n; +assert(nan == nan); +``` + +Both remainder operators follow the same rule, so `5n % 0n` and `5n %% 0n` are +`NaN` where `5 % 0` and `5 %% 0` are errors. Moving an accumulator from `Int` +to `Number` therefore trades the zero-divisor diagnostic for a value that +propagates. + +## Equality, hashing, and ordering + +Numeric equality compares exact values across all three modes. Equal values produce the same map or set hash. Andy C++ converts each finite Float to its exact binary rational value for this comparison, so decimal approximation does not make values equal: + +```ndc +assert(1 == 1.0); +assert(1.0 == 1n); +assert(1n == 1 + 0i); +assert(0.1 != 1n / 10n); +assert_eq(%{1, 1.0, 1n, 1 + 0i}.len(), 1); +``` + +Positive and negative zero compare equal. All NaN values compare equal and hash alike. Real scalars sort in this order: + +```text +-Inf < finite values < Inf < NaN +``` + +Complex values keep lexicographic ordering. The comparison checks the real part, then the imaginary part: + +```ndc +assert((2 + 0i) > (1 + 100i)); +assert((1 + 2i) < (1 + 3i)); +``` + +## Integer-only operators + +Only `Int` supports bitwise operations and shifts: + +| Operator | Function | +| :-: | --- | +| `\|` | Bitwise OR | +| `&` | Bitwise AND | +| `~` | Binary XOR or unary NOT | +| `>>` | Checked right shift | +| `<<` | Checked left shift | + +Use `Int` values for list indices, range bounds, and APIs that take counts. Convert with `int(value)` when the value fits in `i64`. diff --git a/manual/src/reference/types/struct.md b/manual/src/reference/types/struct.md index b91c40cc..8d40b5b1 100644 --- a/manual/src/reference/types/struct.md +++ b/manual/src/reference/types/struct.md @@ -154,11 +154,10 @@ assert_eq(c.hits, 5); ``` A field is a typed location, so an augmented assignment whose result would not -fit the field type is rejected. For example `/` and `^` on integers may produce -non-integer numbers: +fit the field type is rejected: ```ndc -c.hits /= 2; // ERROR: mismatched types: found Number but expected Int +c.hits += 0.5; // ERROR: mismatched types: found Float but expected Int ``` ## Reference semantics diff --git a/manual/src/reference/variables-and-scopes.md b/manual/src/reference/variables-and-scopes.md index b8ff7f14..933a0124 100644 --- a/manual/src/reference/variables-and-scopes.md +++ b/manual/src/reference/variables-and-scopes.md @@ -45,9 +45,18 @@ print(x); // 3 The `=` operator can be used to reassign a value to an existing variable. When you reassign a variable to a value of a different type, the variable's type is widened to the least upper bound (LUB) of the old and new types. ```ndc -let x = 1; // type is Int -x = 2; // type is still Int -x = 3.14; // type widens to Number (LUB of Int and Float) +let x = 1; // the initializer and declaration have type Int +x = 2; // subsequent reads still have type Int +x = 3.14; // subsequent reads have type Any (Int and Float are siblings) +x; // type is Any +``` + +Augmented assignment widens an inferred binding from the operation's result in the same way: + +```ndc +let total = 3; +total += 0.5; // Int + Float returns Float, so subsequent reads have type Any +total; // type is Any ``` ```ndc @@ -68,11 +77,11 @@ let name: String = "world"; let xs: List = [1, 2, 3]; ``` -A subtype is fine — `Int` fits where `Number` is asked for, and so on: +A subtype is fine. All concrete types fit where `Any` is requested: ```ndc -let n: Number = 3; // OK: Int is a Number -let x: Any = "anything"; // OK: everything is Any +let n: Number = 3n; +let x: Any = "anything"; // every value fits Any ``` A mismatch is rejected with a `mismatched types` error: @@ -95,7 +104,7 @@ Once a binding has an annotation, it stays locked to that type. Reassignment and ```ndc let x: Int = 5; x = "test"; // ERROR: mismatched types -x /= 2; // ERROR: division can produce a Rational, which doesn't fit in Int +x += 0.5; // ERROR: the Float result doesn't fit in Int ``` If you want a binding that widens freely, just leave the annotation off. Annotations are opt-in. diff --git a/manual/src/troubleshooting/overload-dispatch-collections.md b/manual/src/troubleshooting/overload-dispatch-collections.md index 78e45616..d68c04d6 100644 --- a/manual/src/troubleshooting/overload-dispatch-collections.md +++ b/manual/src/troubleshooting/overload-dispatch-collections.md @@ -41,20 +41,48 @@ fn process(List) would both fail to match under dynamic dispatch if the list type cannot be resolved at compile time, because verifying element types would require scanning the entire container. -In practice this limitation is not currently visible: +Standard library overloads do differ by element type. Numeric sequence functions preserve the +concrete numeric type, so `sum` has three typed overloads: -- User-defined functions cannot yet declare typed container parameters (the syntax is not - implemented), so user overloads always use `Any` and dispatch works correctly. -- All standard library overloads on container parameters use `` element types - (e.g. `List`, `Sequence`, `Map`), so they also hit the fast path. - Overloads that differ by container *kind* (e.g. `pop(List)` vs `pop(MinHeap)`) - are distinguished by the container kind check alone. +``` +fn sum(Sequence) -> Int +fn sum(Sequence) -> Float +fn sum(Sequence) -> Number +``` + +A value the analyser only knows as `List` matches none of them. Rather than dispatch on +it and scan, the call is rejected at compile time: + +```ndc +let values: List = [1, 2, 3]; +values.sum() +// error[resolver]: No function called 'sum' found that matches the arguments 'List' +// = An overload would accept a narrower argument type. Cast to say what the value holds, +// as in `value as List`. +``` + +Overloads that differ only by container *kind* (e.g. `pop(List)` vs `pop(MinHeap)`) +are still distinguished by the kind check alone. User-defined functions cannot yet declare +typed container parameters (the syntax is not implemented), so user overloads always use +`Any`. ## Workaround -If you notice that a function call unexpectedly fails to match an overload, move the call to a -location where Andy C++ can infer the argument types statically — for example, directly at the -call site rather than through an intermediate untyped function parameter: +State the element type with a [cast](../features/casts.md). A cast scans the value, but the +cost lands at a site you wrote rather than inside every dispatch: + +```ndc +let values = %{}; +values.insert(1); +values.insert(2); + +// keys is typed List; the cast recovers Int so sum can resolve. +assert_eq((values.keys as List).sum(), 3); +``` + +Otherwise, move the call to a location where Andy C++ can infer the argument types +statically — for example, directly at the call site rather than through an intermediate +untyped function parameter: ```ndc // The type of `data` is Any here — dynamic dispatch used diff --git a/ndc_analyser/src/analyser.rs b/ndc_analyser/src/analyser.rs index c2c0e1a8..37f62942 100644 --- a/ndc_analyser/src/analyser.rs +++ b/ndc_analyser/src/analyser.rs @@ -3,7 +3,7 @@ use itertools::{Itertools, izip}; use ndc_core::static_type::StaticTypeConstructionError; use ndc_core::r#struct::StructRegistry; use ndc_core::{StaticType, TypeSignature}; -use ndc_lexer::Span; +use ndc_lexer::{NumericLiteral, Span}; use ndc_parser::{ AugmentedAssignmentPlan, Binding, Candidate, Expression, ExpressionLocation, ForBody, ForIteration, FunctionParameter, Lvalue, NodeId, TypeExpr, @@ -255,9 +255,13 @@ impl Analyser { match expression { Expression::BoolLiteral(_) => Ok(StaticType::Bool), Expression::StringLiteral(_) => Ok(StaticType::String), - Expression::Int64Literal(_) | Expression::BigIntLiteral(_) => Ok(StaticType::Int), - Expression::Float64Literal(_) => Ok(StaticType::Float), - Expression::ComplexLiteral(_) => Ok(StaticType::Complex), + Expression::NumericLiteral(NumericLiteral::Int64(_)) => Ok(StaticType::Int), + Expression::NumericLiteral(NumericLiteral::Float64(_)) => Ok(StaticType::Float), + Expression::NumericLiteral( + NumericLiteral::NumberInt(_) + | NumericLiteral::NumberFloat(_) + | NumericLiteral::Complex(_), + ) => Ok(StaticType::Number), Expression::Continue | Expression::Break => Ok(StaticType::Never), Expression::Identifier { name: ident, @@ -1521,7 +1525,34 @@ mod tests { "let values = [1]; values[0] += 0.5; values", vec![("+".to_string(), add)], ), - StaticType::List(Box::new(StaticType::Number)), + StaticType::List(Box::new(StaticType::Any)), + ); + } + + #[test] + fn inferred_identifier_assignments_widen_subsequent_reads() { + assert_eq!(analyse_last_type("let x = 3; x = 0.5; x"), StaticType::Any,); + + let add = StaticType::Function { + parameters: Some(vec![StaticType::Int, StaticType::Float]), + return_type: Box::new(StaticType::Float), + }; + assert_eq!( + analyse_last_type_with_globals("let x = 3; x += 0.5; x", vec![("+".to_string(), add)],), + StaticType::Any, + ); + } + + #[test] + fn annotated_identifier_augmented_assignment_rejects_widening() { + let add = StaticType::Function { + parameters: Some(vec![StaticType::Int, StaticType::Float]), + return_type: Box::new(StaticType::Float), + }; + assert_analysis_error( + "let x: Int = 3; x += 0.5;", + vec![("+".to_string(), add)], + "mismatched types: found Float but expected Int", ); } diff --git a/ndc_analyser/src/scope.rs b/ndc_analyser/src/scope.rs index 950a8094..4e57e299 100644 --- a/ndc_analyser/src/scope.rs +++ b/ndc_analyser/src/scope.rs @@ -1469,9 +1469,8 @@ mod tests { assert_resolves_to(&mut tree, "locate", &call_sig, 0); } - // From the spec: distance(fn(Int) -> String, fn(Number) -> String) = 1. - // When two overloads in the same scope both match, the one whose params - // are a strict subtype (here `Int` is a subtype of `Number`) wins. + // Int and Number are siblings. An Int call must select the Int overload + // without treating the Number overload as a fallback candidate. #[test] fn more_specific_numeric_overload_wins() { let mut tree = ScopeTree::from_global_scope(vec![ diff --git a/ndc_bin/src/highlighter.rs b/ndc_bin/src/highlighter.rs index 9696660a..21834fc1 100644 --- a/ndc_bin/src/highlighter.rs +++ b/ndc_bin/src/highlighter.rs @@ -56,13 +56,9 @@ impl AndycppHighlighter { // Strings — green Token::String(_) => substring.rgb(152, 195, 121), // Numeric literals and booleans — orange - Token::BigInt(_) - | Token::Int64(_) - | Token::Float64(_) - | Token::Complex(_) - | Token::Infinity - | Token::True - | Token::False => substring.rgb(209, 154, 102), + Token::NumericLiteral(_) | Token::True | Token::False => { + substring.rgb(209, 154, 102) + } // Keywords — coral red Token::Let | Token::Fn @@ -243,13 +239,35 @@ fn collect_function_spans(expr: &ExpressionLocation, spans: &mut AHashSet // Leaves — no sub-expressions to recurse into Expression::BoolLiteral(_) | Expression::StringLiteral(_) - | Expression::Int64Literal(_) - | Expression::Float64Literal(_) - | Expression::BigIntLiteral(_) - | Expression::ComplexLiteral(_) + | Expression::NumericLiteral(_) | Expression::Identifier { .. } | Expression::StructDeclaration { .. } | Expression::Break | Expression::Continue => {} } } + +#[cfg(test)] +mod tests { + use super::*; + use yansi::Color; + + #[test] + fn number_literals_are_highlighted_as_numbers() { + for literal in ["42n", "1.25n", "0b101n", "0o77n", "0xffn"] { + let highlighted = AndycppHighlighter::highlight_parsed(literal); + + assert_eq!( + highlighted.len(), + 1, + "unexpected tokenization for {literal}" + ); + assert_eq!(highlighted[0].value, literal); + assert_eq!( + highlighted[0].style.foreground, + Some(Color::Rgb(209, 154, 102)), + "unexpected color for {literal}" + ); + } + } +} diff --git a/ndc_core/Cargo.toml b/ndc_core/Cargo.toml index 61c174c3..66a05052 100644 --- a/ndc_core/Cargo.toml +++ b/ndc_core/Cargo.toml @@ -6,7 +6,3 @@ version.workspace = true [dependencies] ahash.workspace = true itertools.workspace = true -num.workspace = true -ordered-float.workspace = true -ryu.workspace = true -thiserror.workspace = true diff --git a/ndc_core/src/int.rs b/ndc_core/src/int.rs deleted file mode 100644 index 1b583013..00000000 --- a/ndc_core/src/int.rs +++ /dev/null @@ -1,461 +0,0 @@ -use num::FromPrimitive; -use num::bigint::{Sign, ToBigInt}; -use num::complex::Complex64; -use num::traits::CheckedEuclid; -use num::{BigInt, BigRational, Signed, ToPrimitive, Zero, pow::Pow}; -use std::cmp::Ordering; -use std::fmt; -use std::hash::{Hash, Hasher}; -use std::ops::{BitAnd, BitOr, BitXor, Neg, Not, Shl, Shr}; - -#[derive(Debug, Clone)] -pub enum Int { - Int64(i64), - BigInt(BigInt), -} - -impl Int { - /// Truncates the float and returns Some if the float was not NaN or Inf - #[must_use] - pub fn from_f64_trunc(value: f64) -> Option { - if value.is_nan() || value.is_infinite() { - return None; - } - - BigInt::from_f64(value).map(Int::BigInt) - } - - /// Converts to float to an int only if it's fractal part is zero, otherwise it returns None - #[must_use] - pub fn from_f64_if_int(value: f64) -> Option { - if value.fract() == 0.0 { - Self::from_f64_trunc(value) - } else { - None - } - } - - #[must_use] - pub fn to_bigint(&self) -> BigInt { - match self { - Self::Int64(i) => BigInt::from(*i), - Self::BigInt(b) => b.clone(), - } - } - - #[must_use] - pub fn simplified(self) -> Self { - match self { - i @ Self::Int64(_) => i, - Self::BigInt(bi) => { - if let Some(i) = bi.to_i64() { - Self::Int64(i) - } else { - Self::BigInt(bi) - } - } - } - } - - pub fn checked_rem_euclid(self, rhs: &Self) -> Option { - if let (Self::Int64(p1), Self::Int64(p2)) = (&self, &rhs) - && let Some(a) = (*p1).checked_rem_euclid(*p2) - { - return Some(Self::Int64(a)); - } - - self.to_bigint() - .checked_rem_euclid(&rhs.to_bigint()) - .map(Int::BigInt) - } - - #[must_use] - pub fn checked_shl(self, rhs: Self) -> Option { - let rhs: u32 = match rhs { - Self::Int64(rhs) => rhs.try_into().ok()?, - Self::BigInt(rhs) => rhs.try_into().ok()?, - }; - - match self { - Self::Int64(lhs) => { - if let Some(result) = lhs.checked_shl(rhs) { - Some(Self::Int64(result)) - } else { - Some(Self::BigInt(lhs.to_bigint().expect("cannot fail").shl(rhs))) - } - } - Self::BigInt(big_int) => Some(Self::BigInt(big_int.shl(rhs))), - } - } - - #[must_use] - pub fn checked_shr(self, rhs: Self) -> Option { - let rhs: u32 = match rhs { - Self::Int64(rhs) => rhs.try_into().ok()?, - Self::BigInt(rhs) => rhs.try_into().ok()?, - }; - - match self { - Self::Int64(lhs) => { - if let Some(result) = lhs.checked_shr(rhs) { - Some(Self::Int64(result)) - } else { - Some(Self::BigInt(lhs.to_bigint().expect("cannot fail").shr(rhs))) - } - } - Self::BigInt(big_int) => Some(Self::BigInt(big_int.shr(rhs))), - } - } - - /// # Panics - /// This method panics if the `rhs` argument is negative - #[must_use] - pub fn pow(&self, rhs: &Self) -> Self { - let lhs = self.to_bigint(); - let rhs = rhs.to_bigint(); - match rhs.sign() { - Sign::Minus => panic!("right hand side must not be negative"), - Sign::NoSign => Self::Int64(1), - // This is kinda dumb because if the `rhs` doesn't fit in a u64 we sure as hell aren't - // going to be able to compute it in finite time - Sign::Plus => Self::BigInt(Pow::pow(lhs, rhs.magnitude())), - } - } - - #[must_use] - pub fn is_negative(&self) -> bool { - match self { - Self::Int64(i) => i.is_negative(), - Self::BigInt(i) => i.is_negative(), - } - } - - #[must_use] - pub fn is_zero(&self) -> bool { - match self { - Self::Int64(i) => i.is_zero(), - Self::BigInt(i) => i.is_zero(), - } - } - - #[must_use] - pub fn is_positive(&self) -> bool { - match self { - Self::Int64(i) => i.is_positive(), - Self::BigInt(i) => i.is_positive(), - } - } - - #[must_use] - pub fn abs(&self) -> Self { - match self { - // `i64::MIN.abs()` overflows, so promote to BigInt in that case. - Self::Int64(i) => i - .checked_abs() - .map_or_else(|| Self::from(BigInt::from(*i).abs()), Self::Int64), - Self::BigInt(b) => Self::from(b.abs()), - } - } - - #[must_use] - pub fn signum(&self) -> Self { - match self { - Self::Int64(i) => Self::Int64(i.signum()), - Self::BigInt(i) => match i.sign() { - Sign::Minus => Self::Int64(-1), - Sign::NoSign => Self::Int64(0), - Sign::Plus => Self::Int64(1), - }, - } - } -} - -impl PartialEq for Int { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::BigInt(left), Self::BigInt(right)) => left == right, - (Self::BigInt(left), Self::Int64(right)) => left == &BigInt::from(*right), - (Self::Int64(left), Self::BigInt(right)) => &BigInt::from(*left) == right, - (Self::Int64(left), Self::Int64(right)) => left == right, - } - } -} - -impl Eq for Int {} - -impl Ord for Int { - fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (Self::Int64(l), Self::BigInt(r)) => BigInt::from(*l).cmp(r), - (Self::BigInt(l), Self::Int64(r)) => l.cmp(&BigInt::from(*r)), - (Self::Int64(l), Self::Int64(r)) => l.cmp(r), - (Self::BigInt(l), Self::BigInt(r)) => l.cmp(r), - } - } -} - -impl Hash for Int { - // This hash implementation ensures that a BigInt that fits in an i64 is hashed the same way an i64 would - fn hash(&self, state: &mut H) { - match self { - Self::Int64(i) => state.write_i64(*i), - Self::BigInt(b) => { - if let Some(i) = b.to_i64() { - state.write_i64(i); - } else { - b.hash(state); - } - } - } - } -} - -impl PartialOrd for Int { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Neg for Int { - type Output = Self; - - fn neg(self) -> Self::Output { - match self { - // `-i64::MIN` overflows, so promote to BigInt in that case. - Self::Int64(i) => i - .checked_neg() - .map_or_else(|| Self::BigInt(BigInt::from(i).neg()), Self::Int64), - Self::BigInt(i) => Self::BigInt(i.neg()), - } - } -} - -impl Not for Int { - type Output = Self; - - fn not(self) -> Self::Output { - match self { - Self::Int64(i) => i.not().into(), - Self::BigInt(big_int) => big_int.not().into(), - } - } -} - -macro_rules! impl_binary_operator { - ($trait:ident, $method:ident, $safe_method:ident) => { - impl std::ops::$trait for Int { - type Output = Int; - - fn $method(self, rhs: Self) -> Self::Output { - match (&self, &rhs) { - (Int::Int64(p1), Int::Int64(p2)) => { - if let Some(s) = p1.$safe_method(*p2) { - return Int::Int64(s); - } - } - _ => (), - } - Int::BigInt(self.to_bigint().$method(rhs.to_bigint())) - } - } - impl std::ops::$trait<&Int> for Int { - type Output = Int; - - fn $method(self, rhs: &Self) -> Self::Output { - match (&self, rhs) { - (Int::Int64(p1), Int::Int64(p2)) => { - if let Some(s) = p1.$safe_method(*p2) { - return Int::Int64(s); - } - } - _ => (), - } - Int::BigInt(self.to_bigint().$method(rhs.to_bigint())) - } - } - impl std::ops::$trait for &Int { - type Output = Int; - - fn $method(self, rhs: Int) -> Self::Output { - match (&self, &rhs) { - (Int::Int64(p1), Int::Int64(p2)) => { - if let Some(s) = p1.$safe_method(*p2) { - Int::Int64(s); - } - } - _ => {} - } - - Int::BigInt(self.to_bigint().$method(rhs.to_bigint())) - } - } - impl std::ops::$trait<&Int> for &Int { - type Output = Int; - - fn $method(self, rhs: &Int) -> Self::Output { - match (self, rhs) { - (Int::Int64(p1), Int::Int64(p2)) => { - if let Some(s) = p1.$safe_method(*p2) { - return Int::Int64(s); - } - } - _ => (), - } - Int::BigInt(self.to_bigint().$method(rhs.to_bigint())) - } - } - }; -} -impl_binary_operator!(Add, add, checked_add); -impl_binary_operator!(Sub, sub, checked_sub); -impl_binary_operator!(Mul, mul, checked_mul); -impl_binary_operator!(Div, div, checked_div); - -impl_binary_operator!(Rem, rem, checked_rem); - -impl BitAnd for &Int { - type Output = Int; - - fn bitand(self, rhs: Self) -> Self::Output { - if let (Int::Int64(p1), Int::Int64(p2)) = (self, rhs) { - return Int::Int64(p1 & p2); - } - - Int::BigInt(self.to_bigint().bitand(rhs.to_bigint())).simplified() - } -} - -impl BitAnd for Int { - type Output = Self; - - fn bitand(self, rhs: Self) -> Self::Output { - &self & &rhs - } -} - -impl BitOr for &Int { - type Output = Int; - - fn bitor(self, rhs: Self) -> Self::Output { - if let (Int::Int64(p1), Int::Int64(p2)) = (self, rhs) { - return Int::Int64(p1 | p2); - } - - Int::BigInt(self.to_bigint().bitor(rhs.to_bigint())) - } -} - -impl BitOr for Int { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self::Output { - &self | &rhs - } -} - -impl BitXor for &Int { - type Output = Int; - - fn bitxor(self, rhs: Self) -> Self::Output { - if let (Int::Int64(p1), Int::Int64(p2)) = (self, rhs) { - return Int::Int64(p1 ^ p2); - } - - Int::BigInt(self.to_bigint().bitxor(rhs.to_bigint())).simplified() - } -} - -impl BitXor for Int { - type Output = Self; - - fn bitxor(self, rhs: Self) -> Self::Output { - &self ^ &rhs - } -} - -impl From for Int { - fn from(value: i32) -> Self { - Self::Int64(i64::from(value)) - } -} - -impl From for Int { - fn from(value: i64) -> Self { - Self::Int64(value) - } -} - -impl From for Int { - fn from(value: BigInt) -> Self { - Self::BigInt(value) - } -} - -impl From for f64 { - fn from(value: Int) -> Self { - Self::from(&value) - } -} - -impl From<&Int> for f64 { - fn from(value: &Int) -> Self { - match value { - Int::Int64(i) => i.to_f64().unwrap_or(Self::INFINITY), - Int::BigInt(i) => i.to_f64().unwrap_or(Self::INFINITY), - } - } -} - -impl From for BigInt { - fn from(value: Int) -> Self { - match value { - Int::Int64(i) => Self::from(i), - Int::BigInt(b) => b, - } - } -} - -impl From<&Int> for BigInt { - fn from(value: &Int) -> Self { - match value { - Int::Int64(i) => Self::from(*i), - Int::BigInt(b) => b.clone(), - } - } -} - -impl From for BigRational { - fn from(value: Int) -> Self { - Self::from(value.to_bigint()) - } -} - -impl From<&Int> for BigRational { - fn from(value: &Int) -> Self { - Self::from(value.to_bigint()) - } -} - -impl From for Complex64 { - fn from(value: Int) -> Self { - Self::from(&value) - } -} - -impl From<&Int> for Complex64 { - fn from(value: &Int) -> Self { - match value { - Int::Int64(i) => Self::from(i.to_f64().unwrap_or(f64::INFINITY)), - Int::BigInt(i) => Self::from(i.to_f64().unwrap_or(f64::INFINITY)), - } - } -} - -impl fmt::Display for Int { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Int64(i) => write!(f, "{i}"), - Self::BigInt(b) => write!(f, "{b}"), - } - } -} diff --git a/ndc_core/src/lib.rs b/ndc_core/src/lib.rs index cf869a43..eda30d70 100644 --- a/ndc_core/src/lib.rs +++ b/ndc_core/src/lib.rs @@ -1,8 +1,6 @@ pub mod compare; pub mod duration; pub mod hash_map; -pub mod int; -pub mod num; pub mod static_type; pub mod r#struct; diff --git a/ndc_core/src/num.rs b/ndc_core/src/num.rs deleted file mode 100644 index ab3bba33..00000000 --- a/ndc_core/src/num.rs +++ /dev/null @@ -1,804 +0,0 @@ -use std::cmp::Ordering; -use std::fmt; -use std::hash::{Hash, Hasher}; -use std::num::TryFromIntError; -use std::ops::{Add, Div, Mul, Neg, Not, Rem, Sub}; - -use crate::StaticType; -use crate::int::Int; -use num::bigint::TryFromBigIntError; -use num::complex::{Complex64, ComplexFloat}; -use num::{BigInt, BigRational, Complex, FromPrimitive, Signed, ToPrimitive, Zero}; -use ordered_float::OrderedFloat; - -#[derive(Debug, Clone)] -pub enum Number { - Int(Int), - Float(f64), - Rational(Box), - Complex(Complex64), -} - -#[derive(Debug)] -pub enum RealNumber<'a> { - Int(&'a Int), - Float(OrderedFloat), -} - -impl From for Number { - fn from(value: Int) -> Self { - Self::Int(value) - } -} - -impl From for Number { - fn from(value: i32) -> Self { - Self::Int(Int::from(value)) - } -} - -impl From for Number { - fn from(value: f64) -> Self { - Self::Float(value) - } -} - -impl From for Number { - fn from(value: BigRational) -> Self { - Self::Rational(Box::new(value)) - } -} - -impl From for Number { - fn from(value: Complex64) -> Self { - Self::Complex(value) - } -} - -impl PartialOrd for Number { - fn partial_cmp(&self, other: &Self) -> Option { - self.to_reals().partial_cmp(&other.to_reals()) - } -} - -impl PartialEq for Number { - fn eq(&self, other: &Self) -> bool { - self.partial_cmp(other) == Some(Ordering::Equal) - } -} - -impl Default for Number { - fn default() -> Self { - Self::Int(Int::Int64(0)) - } -} - -impl Hash for Number { - fn hash(&self, state: &mut H) { - match self { - Self::Int(i) => { - state.write_u8(1); - i.hash(state); - } - Self::Float(f) => { - // If this float happens to be an integer (such as 1.0) hash it as if it's an int - match Int::from_f64_if_int(*f) { - None => { - state.write_u8(2); - OrderedFloat(*f).hash(state); - } - Some(int) => { - state.write_u8(1); - int.hash(state); - } - } - } - Self::Rational(r) => { - if r.is_integer() { - // simplify rational - state.write_u8(1); - Int::BigInt(r.to_integer()).hash(state); - } else { - state.write_u8(3); - r.hash(state); - } - } - Self::Complex(c) => { - state.write_u8(4); - OrderedFloat(c.re).hash(state); - OrderedFloat(c.im).hash(state); - } - } - } -} - -impl PartialEq for RealNumber<'_> { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Int(left), Self::Int(right)) => left.eq(right), - (Self::Float(left), Self::Float(right)) => left.eq(right), - (Self::Int(left), Self::Float(right)) => { - compare_int_to_float(left, *right) == Some(Ordering::Equal) - } - (Self::Float(left), Self::Int(right)) => { - compare_int_to_float(right, *left) == Some(Ordering::Equal) - } - } - } -} - -fn compare_int_to_float(a: &Int, b: OrderedFloat) -> Option { - if b.is_infinite() { - if b.is_sign_positive() { - Some(Ordering::Less) - } else { - Some(Ordering::Greater) - } - } else if b.is_nan() { - Some(OrderedFloat(f64::from(a)).cmp(&b)) - } else { - let x = BigInt::from_f64(b.trunc()).expect("b can't be NaN"); - a.to_bigint() - .partial_cmp(&x) - .map(|ord| ord.then(0.0f64.total_cmp(&b.fract()))) - } -} - -impl PartialOrd for RealNumber<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (RealNumber::Int(a), RealNumber::Int(b)) => Some(a.cmp(b)), - (RealNumber::Int(a), RealNumber::Float(b)) => compare_int_to_float(a, *b), - (RealNumber::Float(a), RealNumber::Int(b)) => { - compare_int_to_float(b, *a).map(Ordering::reverse) - } - (RealNumber::Float(a), RealNumber::Float(b)) => { - Some(OrderedFloat(*a).cmp(&OrderedFloat(*b))) - } - } - } -} - -impl Neg for Number { - type Output = Self; - - fn neg(self) -> Self::Output { - match self { - Self::Int(i) => i.neg().into(), - Self::Float(f) => f.neg().into(), - Self::Rational(r) => r.neg().into(), - Self::Complex(c) => c.neg().into(), - } - } -} - -impl Not for Number { - type Output = Self; - - fn not(self) -> Self::Output { - match self { - Self::Int(int) => int.not().into(), - _ => f64::NAN.into(), - } - } -} - -trait Unbox { - type Output; - fn unbox(self) -> Self::Output; -} - -impl Unbox for Box { - type Output = BigRational; - fn unbox(self) -> Self::Output { - *self - } -} - -impl<'a> Unbox for &'a Box { - type Output = &'a BigRational; - fn unbox(self) -> Self::Output { - &**self - } -} - -#[derive(Debug)] -pub struct BinaryOperatorError(String); - -impl fmt::Display for BinaryOperatorError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for BinaryOperatorError {} - -impl BinaryOperatorError { - pub fn new(message: String) -> Self { - Self(message) - } - - pub fn undefined_operation(operator: &str, left: &StaticType, right: &StaticType) -> Self { - Self(format!( - "operator {operator} is not defined for {left} and {right}" - )) - } -} - -macro_rules! impl_binary_operator { - ($self:ty, $other:ty, $trait:ident, $method:ident,$intmethod:expr,$floatmethod:expr,$rationalmethod:expr,$complexmethod:expr) => { - impl $trait<$other> for $self { - type Output = Result; - fn $method(self, other: $other) -> Self::Output { - Ok(match (self, other) { - // Integer - (Number::Int(left), Number::Int(right)) => Number::Int($intmethod(left, right)), - // Complex - (Number::Complex(left), right) => { - Number::Complex($complexmethod(left, right.to_complex())) - } - (left, Number::Complex(right)) => { - Number::Complex($complexmethod(left.to_complex(), right)) - } - // Float - // NOTE: these `expect` calls are safe because complex has already been handled - (Number::Float(left), right) => Number::Float($floatmethod( - left, - right.to_f64().expect("cannot convert complex to float"), - )), - (left, Number::Float(right)) => Number::Float($floatmethod( - left.to_f64().expect("cannot convert complex to float"), - right, - )), - // Rational - // NOTE: these `expect` calls are safe because complex and float are handled - (left, Number::Rational(right)) => Number::rational($rationalmethod( - left.to_rational().expect("cannot convert to rational"), - right.unbox(), - )), - (Number::Rational(left), right) => Number::rational($rationalmethod( - left.unbox(), - right.to_rational().expect("cannot convert to rational"), - )), - }) - } - } - }; -} - -macro_rules! impl_binary_operator_all { - ($implement:ident,$method:ident,$intmethod:expr,$floatmethod:expr,$rationalmethod:expr,$complexmethod:expr) => { - impl_binary_operator!( - Number, - Number, - $implement, - $method, - $intmethod, - $floatmethod, - $rationalmethod, - $complexmethod - ); - impl_binary_operator!( - Number, - &Number, - $implement, - $method, - $intmethod, - $floatmethod, - $rationalmethod, - $complexmethod - ); - impl_binary_operator!( - &Number, - Number, - $implement, - $method, - $intmethod, - $floatmethod, - $rationalmethod, - $complexmethod - ); - impl_binary_operator!( - &Number, - &Number, - $implement, - $method, - $intmethod, - $floatmethod, - $rationalmethod, - $complexmethod - ); - }; -} - -impl_binary_operator_all!(Add, add, Add::add, Add::add, Add::add, Add::add); -impl_binary_operator_all!(Sub, sub, Sub::sub, Sub::sub, Sub::sub, Sub::sub); -impl_binary_operator_all!(Mul, mul, Mul::mul, Mul::mul, Mul::mul, Mul::mul); - -/// Returns `true` for the number kinds that use exact (integer/rational) -/// arithmetic. Remainder of an exact value by zero panics in `num` and -/// `num-bigint`; floats and complex numbers produce `NaN` instead, so they -/// are handled by the normal arithmetic path. -fn is_exact(n: &Number) -> bool { - matches!(n, Number::Int(_) | Number::Rational(_)) -} - -impl Rem for Number { - type Output = Result; - - fn rem(self, rhs: Self) -> Self::Output { - // Reject exact remainder by zero up front; without this the integer - // and rational arms below panic ("attempt to divide by zero"). - if is_exact(&self) && is_exact(&rhs) && rhs.is_zero() { - return Err(BinaryOperatorError::new("division by zero".to_string())); - } - Ok(match (self, rhs) { - // Integer - (Self::Int(left), Self::Int(right)) => Self::Int(left % right), - // Complex - (Self::Complex(left), right) => Self::Complex(left % right.to_complex()), - (left, Self::Complex(right)) => Self::Complex(left.to_complex() % right), - // Float - (Self::Float(left), right) => { - Self::Float(left % right.to_f64().expect("cannot convert complex to float")) - } - (left, Self::Float(right)) => { - Self::Float(left.to_f64().expect("cannot convert complex to float") % right) - } - // Rational - (left, Self::Rational(right)) => Self::rational( - left.to_rational().expect("cannot convert to rational") % right.unbox(), - ), - (Self::Rational(left), right) => Self::rational( - left.unbox() % right.to_rational().expect("cannot convert to rational"), - ), - }) - } -} - -impl Rem<&Self> for Number { - type Output = Result; - - fn rem(self, rhs: &Self) -> Self::Output { - self % rhs.clone() - } -} - -impl Rem for &Number { - type Output = Result; - - fn rem(self, rhs: Number) -> Self::Output { - self.clone() % rhs - } -} - -impl Rem<&Number> for &Number { - type Output = Result; - - fn rem(self, rhs: &Number) -> Self::Output { - self.clone() % rhs.clone() - } -} - -impl Div<&Number> for &Number { - type Output = Number; - - fn div(self, rhs: &Number) -> Self::Output { - match (self.to_rational(), rhs.to_rational()) { - (Some(left), Some(right)) if !right.is_zero() => Number::rational(left / right), - _ => match (self.to_f64(), rhs.to_f64()) { - (Some(left), Some(right)) => Number::Float(left / right), - _ => Number::Complex(self.to_complex() / rhs.to_complex()), - }, - } - } -} - -impl Div for Number { - type Output = Result; - - fn div(self, rhs: Self) -> Self::Output { - Ok(&self / &rhs) - } -} -impl Div<&Self> for Number { - type Output = Result; - - fn div(self, rhs: &Self) -> Self::Output { - Ok(&self / rhs) - } -} -impl Div for &Number { - type Output = Result; - - fn div(self, rhs: Number) -> Self::Output { - Ok(self / &rhs) - } -} - -impl Number { - #[must_use] - pub fn complex(re: f64, im: f64) -> Self { - Self::Complex(Complex64 { re, im }) - } - - #[must_use] - pub fn float(f: f64) -> Self { - Self::Float(f) - } - - #[must_use] - pub fn rational(rat: BigRational) -> Self { - Self::Rational(Box::new(rat)) - } - - pub fn static_type(&self) -> StaticType { - match self { - Self::Int(_) => StaticType::Int, - Self::Float(_) => StaticType::Float, - Self::Rational(_) => StaticType::Rational, - Self::Complex(_) => StaticType::Complex, - } - } - - #[must_use] - pub fn is_zero(&self) -> bool { - match self { - Self::Int(i) => i.is_zero(), - Self::Float(f) => *f == 0.0, - Self::Rational(r) => r.is_zero(), - Self::Complex(c) => c.is_zero(), - } - } - - pub fn checked_rem_euclid(self, rhs: Self) -> Result { - match (self, rhs) { - (Self::Int(p1), Self::Int(p2)) => { - if p2.is_zero() { - return Err(BinaryOperatorError::new("division by zero".to_string())); - } - p1.checked_rem_euclid(&p2) - .ok_or(BinaryOperatorError::new("operation failed".to_string())) - .map(Self::Int) - } - - (Self::Float(p1), Self::Float(p2)) => Ok(Self::Float(p1.rem_euclid(p2))), - (left, right) => Err(BinaryOperatorError::undefined_operation( - "%%", - &left.static_type(), - &right.static_type(), - )), - } - } - - pub fn floor_div(self, rhs: Self) -> Result { - match (self, rhs) { - // Fast path for two i64s. `div_euclid` panics on a zero divisor - // (and on `i64::MIN / -1`), so fall back to the general path in - // those cases — it promotes to a float and yields infinity for a - // zero divisor, matching `/` and the BigInt/rational paths. - (Self::Int(Int::Int64(l)), Self::Int(Int::Int64(r))) => match l.checked_div_euclid(r) { - Some(q) => Ok(Self::Int(Int::Int64(q))), - None => Self::Int(Int::Int64(l)) - .div(Self::Int(Int::Int64(r))) - .map(|n| n.floor()), - }, - (l, r) => Ok(l.div(r)?.floor()), - } - } - - /// Raise an integer base to a (possibly negative) integer exponent. - /// A negative exponent yields the reciprocal as a rational, except - /// `0 ^ negative`, which is division by zero and returns an error - /// instead of panicking with a zero denominator. - fn int_pow(base: &Int, exponent: &Int) -> Result { - if exponent.is_negative() { - if base.is_zero() { - return Err(BinaryOperatorError::new("division by zero".to_string())); - } - let exponent = exponent.to_bigint(); - let denominator = num::pow::Pow::pow(base.to_bigint(), exponent.magnitude()); - Ok(Self::Rational(Box::new(BigRational::new( - BigInt::from(1), - denominator, - )))) - } else { - Ok(Self::Int(base.pow(exponent))) - } - } - - pub fn pow(self, rhs: Self) -> Result { - // Reject astronomically large integer exponents up front: an exponent - // that doesn't fit in u32 would produce a result too large to compute - // in finite time. Without this guard, `2 ^ i64::MAX` hangs the VM. - const MAX_EXPONENT_BITS: u64 = 32; - let too_large = match &rhs { - Self::Int(Int::BigInt(b)) => b.magnitude().bits() > MAX_EXPONENT_BITS, - Self::Rational(p) if p.is_integer() => p.numer().magnitude().bits() > MAX_EXPONENT_BITS, - _ => false, - }; - if too_large { - return Err(BinaryOperatorError::new( - "exponent too large to compute".to_string(), - )); - } - - Ok(match (self, rhs) { - // Int vs others - (Self::Int(p1), Self::Int(p2)) => return Self::int_pow(&p1, &p2), - (Self::Int(p1), Self::Float(p2)) => Self::Float(f64::from(p1).powf(p2)), - (Self::Int(p1), Self::Complex(p2)) => { - Self::Complex(Complex::from(f64::from(p1)).powc(p2)) - } - (Self::Int(p1), Self::Rational(p2)) => { - if p2.is_integer() { - return Self::int_pow(&p1, &Int::BigInt(p2.to_integer())); - } - - Self::Float(f64::from(p1).powf(rational_to_float(&p2))) - } - - // Rational vs Others - (Self::Rational(p1), Self::Int(p2)) => { - Self::Rational(Box::new(num::pow::Pow::pow(&*p1, p2.to_bigint()))) - } - (Self::Rational(p1), Self::Rational(p2)) => { - if p2.is_integer() - && let Some(p2) = p2.to_i32() - { - return Ok(Self::Rational(Box::new(p1.pow(p2)))); - } - - Self::Float(rational_to_float(&p1).powf(rational_to_float(&p2))) - } - (Self::Rational(p1), Self::Float(p2)) => Self::Float(rational_to_float(&p1).powf(p2)), - (Self::Rational(p1), Self::Complex(p2)) => { - Self::Complex(rational_to_complex(&p1).powc(p2)) - } - - // Float vs others - (Self::Float(p1), Self::Float(p2)) => Self::Float(p1.powf(p2)), - (Self::Float(p1), Self::Complex(p2)) => Self::Complex(Complex::from(p1).powc(p2)), - (Self::Float(p1), Self::Int(p2)) => Self::Float(p1.powf(f64::from(p2))), - (Self::Float(p1), Self::Rational(p2)) => Self::Float(p1.powf(rational_to_float(&p2))), - - // Complex vs others - (Self::Complex(p1), Self::Complex(p2)) => Self::Complex(p1.powc(p2)), - (Self::Complex(p1), Self::Float(p2)) => Self::Complex(p1.powc(Complex::from(p2))), - (Self::Complex(p1), Self::Int(p2)) => { - Self::Complex(p1.powc(Complex::from(f64::from(p2)))) - } - (Self::Complex(p1), Self::Rational(p2)) => { - Self::Complex(p1.powc(rational_to_complex(&p2))) - } - }) - } - - /// # Errors - /// Returns a `NumberConversionError` if you try to convert Inf, NaN, or Complex to an int - pub fn to_int_lossy(&self) -> Result { - let n = match self { - Self::Int(i) => Self::Int(i.clone()), - Self::Float(f) => { - if let Some(bi) = BigInt::from_f64(*f) { - Self::Int(Int::BigInt(bi).simplified()) - } else { - return Err(NumberConversionError(format!("cannot convert {f} to int"))); - } - } - Self::Rational(r) => Self::Int(Int::BigInt(r.to_integer()).simplified()), - Self::Complex(c) => { - return Err(NumberConversionError(format!( - "cannot convert complex number {c} to int" - ))); - } - }; - Ok(n) - } - - #[must_use] - pub fn to_complex(&self) -> Complex64 { - match self { - Self::Int(i) => Complex64::from(i), - Self::Float(f) => Complex64::from(f), - Self::Rational(r) => rational_to_complex(r), - Self::Complex(c) => *c, - } - } - - #[must_use] - pub fn to_f64(&self) -> Option { - match self { - Self::Int(i) => Some(f64::from(i)), - Self::Float(f) => Some(*f), - Self::Rational(r) => Some(rational_to_float(r)), - Self::Complex(_) => None, - } - } - - #[must_use] - pub fn to_rational(&self) -> Option { - match self { - Self::Int(i) => Some(BigRational::from(i)), - Self::Rational(r) => Some(BigRational::clone(&**r)), - Self::Float(_) | Self::Complex(_) => None, - } - } - - /// Converts this number into a real (complex) number with the imaginary part set to 0.0 - /// which makes it easy to do comparison on all numbers (and possibly other things) - #[must_use] - pub fn to_reals(&self) -> (RealNumber<'_>, RealNumber<'_>) { - match self { - Self::Int(i) => (RealNumber::Int(i), RealNumber::Float(OrderedFloat(0.0))), - Self::Float(f) => ( - RealNumber::Float(OrderedFloat(*f)), - RealNumber::Float(OrderedFloat(0.0)), - ), - Self::Rational(r) => ( - RealNumber::Float(OrderedFloat(rational_to_float(r))), - RealNumber::Float(OrderedFloat(0.0)), - ), - Self::Complex(c) => ( - RealNumber::Float(OrderedFloat(c.re)), - RealNumber::Float(OrderedFloat(c.im)), - ), - } - } - - #[must_use] - pub fn abs(&self) -> Self { - match self { - Self::Int(i) => Self::Int(i.abs()), - Self::Float(f) => Self::Float(f.abs()), - Self::Rational(r) => Self::Rational(Box::new(r.abs())), - Self::Complex(c) => Self::Float(c.abs()), - } - } - - #[must_use] - pub fn signum(&self) -> Self { - match self { - Self::Int(i) => i.signum().into(), - Self::Float(f) => Self::Float(f.signum()), - Self::Rational(ratio) => Self::from(ratio.signum()), - Self::Complex(complex) => { - // I trust you Brian :crycat: - if complex.re.is_zero() && complex.im.is_zero() { - self.clone() - } else { - Self::Complex(complex / complex.norm()) - } - } - } - } -} - -macro_rules! implement_rounding { - ($method:ident) => { - impl Number { - #[must_use] - pub fn $method(&self) -> Number { - match self { - Number::Int(i) => Number::Int(i.clone()), - Number::Float(f) => { - let f = f.$method(); - if let Some(i) = Int::from_f64_trunc(f) { - Number::Int(i) - } else { - Number::Float(f) - } - } - Number::Rational(r) => Number::Int(Int::BigInt(r.$method().to_integer())), - Number::Complex(c) => Complex::new(c.re.$method(), c.im.$method()).into(), - } - } - } - }; -} - -implement_rounding!(ceil); -implement_rounding!(floor); -implement_rounding!(round); - -#[derive(thiserror::Error, Debug)] -pub enum NumberToUsizeError { - #[error("expected a non-negative integer, got {0}")] - UnsupportedVariant(StaticType), - #[error("expected a non-negative integer, but the value was negative")] - FromIntError(#[from] TryFromIntError), - #[error("this integer is out of range (must be non-negative and small enough to be an index)")] - FromBigIntError(#[from] TryFromBigIntError), -} - -impl TryFrom for usize { - type Error = NumberToUsizeError; - - fn try_from(value: Number) -> Result { - match value { - Number::Int(Int::Int64(i)) => Ok(Self::try_from(i)?), - Number::Int(Int::BigInt(b)) => Ok(Self::try_from(b)?), - n => Err(NumberToUsizeError::UnsupportedVariant(n.static_type())), - } - } -} - -#[derive(thiserror::Error, Debug)] -pub enum NumberToFloatError { - #[error("cannot convert {0} to float")] - UnsupportedType(StaticType), - #[error("cannot convert {0} to float")] - UnsupportedValue(Number), -} - -impl TryFrom<&Number> for f64 { - type Error = NumberToFloatError; - - fn try_from(value: &Number) -> Result { - match value { - Number::Int(Int::BigInt(bi)) => bi.to_f64(), - Number::Int(Int::Int64(i)) => i.to_f64(), - Number::Float(f) => Some(*f), - Number::Rational(r) => r.to_f64(), - _ => return Err(Self::Error::UnsupportedType(value.static_type())), - } - .ok_or_else(|| Self::Error::UnsupportedValue(value.clone())) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum NumberToIntError { - #[error("cannot convert {0} to int")] - UnsupportedType(StaticType), - #[error("cannot convert {0} to int")] - UnsupportedValue(Number), -} - -impl TryFrom<&Number> for i64 { - type Error = NumberToIntError; - - fn try_from(value: &Number) -> Result { - match value { - Number::Int(Int::BigInt(bi)) => bi - .try_into() - .map_err(|_err| NumberToIntError::UnsupportedValue(value.clone())), - Number::Int(Int::Int64(i)) => Ok(*i), - _ => Err(Self::Error::UnsupportedType(value.static_type())), - } - } -} - -impl fmt::Display for Number { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Int(i) => write!(f, "{i}"), - Self::Float(ff) => { - let mut buffer = ryu::Buffer::new(); - f.write_str(buffer.format(*ff)) - } - Self::Rational(r) => write!(f, "{r}"), - Self::Complex(r) => write!(f, "{r}"), - } - } -} - -fn rational_to_float(r: &BigRational) -> f64 { - r.to_f64().unwrap_or(f64::NAN) -} - -fn rational_to_complex(r: &BigRational) -> Complex { - Complex::from(r.to_f64().unwrap_or(f64::NAN)) -} - -#[derive(thiserror::Error, Debug)] -#[error("{0}")] -pub struct NumberConversionError(String); - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -#[deprecated = "use static type instead?"] -pub enum NumberType { - Int, - Float, - Rational, - Complex, -} diff --git a/ndc_core/src/static_type.rs b/ndc_core/src/static_type.rs index 9c9a8c40..4ebd7d46 100644 --- a/ndc_core/src/static_type.rs +++ b/ndc_core/src/static_type.rs @@ -15,41 +15,6 @@ impl Default for TypeSignature { } impl TypeSignature { - /// Matches a list of `ValueTypes` to a type signature. It can return `None` if there is no match or - /// `Some(num)` where num is the sum of the distances of the types. The type `Int`, is distance 1 - /// away from `Number`, and `Number` is 1 distance from `Any`, then `Int` is distance 2 from `Any`. - pub fn calc_type_score(&self, types: &[StaticType]) -> Option { - match self { - Self::Variadic => Some(0), - Self::Exact(signature) => { - if types.len() == signature.len() { - let mut acc = 0; - for (a, b) in types.iter().zip(signature.iter()) { - let dist = if a == &b.type_name { - 0 - } else if a.is_subtype(&b.type_name) { - 1 - } else { - return None; - }; - acc += dist; - } - - return Some(acc); - } - - None - } - } - } - - pub fn arity(&self) -> Option { - match self { - Self::Variadic => None, - Self::Exact(args) => Some(args.len()), - } - } - pub fn from_annotated_bindings(bindings: Vec<(String, Option)>) -> Self { Self::Exact( bindings @@ -106,10 +71,8 @@ pub enum StaticType { Number, Float, Int, - Rational, - Complex, - // Sequences List -> List + // Sequences List -> Sequence Sequence(Box), List(Box), String, @@ -154,8 +117,8 @@ impl StaticType { /// Must stay in sync with its match arms; the unit test /// `builtin_type_names_are_constructible` guards this. pub const BUILTIN_TYPE_NAMES: &'static [&'static str] = &[ - "Any", "Never", "Bool", "Number", "Float", "Int", "Rational", "Complex", "String", - "Option", "Sequence", "List", "Iterator", "MinHeap", "MaxHeap", "Deque", "Tuple", "Map", + "Any", "Never", "Bool", "Number", "Float", "Int", "String", "Option", "Sequence", "List", + "Iterator", "MinHeap", "MaxHeap", "Deque", "Tuple", "Map", ]; pub fn from_name_and_args( @@ -169,8 +132,6 @@ impl StaticType { "Number" => Self::require_no_args(name, &args).map(|_| Self::Number), "Float" => Self::require_no_args(name, &args).map(|_| Self::Float), "Int" => Self::require_no_args(name, &args).map(|_| Self::Int), - "Rational" => Self::require_no_args(name, &args).map(|_| Self::Rational), - "Complex" => Self::require_no_args(name, &args).map(|_| Self::Complex), "String" => Self::require_no_args(name, &args).map(|_| Self::String), "Option" => { Self::require_exactly_one_arg(name, args).map(|elem| Self::Option(Box::new(elem))) @@ -260,7 +221,7 @@ impl StaticType { /// /// # Key Rules /// - `Any` is the top type (supertype of all types) - /// - `Number` > `{Float, Int, Rational, Complex}` + /// - `Int`, `Float`, and `Number` are sibling types /// - `Sequence` > sequence types with element type T /// - Generic types are covariant in their type parameters /// - Function parameters are **contravariant**, returns are **covariant** @@ -288,9 +249,6 @@ impl StaticType { // Reflexivity: every type is a subtype of itself _ if self == other => true, - // Number hierarchy: all numeric types are subtypes of Number - (Self::Float | Self::Int | Self::Rational | Self::Complex, Self::Number) => true, - // Sequence hierarchy: specific sequences are subtypes of Sequence // where T is their element type (covariant) (Self::List(s), Self::Sequence(t)) @@ -377,8 +335,8 @@ impl StaticType { /// The LUB is the most specific type that is a supertype of both inputs. /// /// # Examples - /// - `lub(Int, Float) = Number` - /// - `lub(List, List) = List` + /// - `lub(Int, Float) = Any` + /// - `lub(List, List) = List` /// - `lub(List, Iterator) = Sequence` /// - `lub(Int, String) = Any` pub fn lub(&self, other: &Self) -> Self { @@ -409,12 +367,6 @@ impl StaticType { } match (self, other) { - // Number type lattice: all numeric types join to Number - (Self::Float, Self::Int | Self::Rational | Self::Complex) - | (Self::Int, Self::Float | Self::Rational | Self::Complex) - | (Self::Rational, Self::Float | Self::Int | Self::Complex) - | (Self::Complex, Self::Float | Self::Int | Self::Rational) => Self::Number, - // Covariant generic types: compute LUB pointwise (Self::Option(s), Self::Option(t)) => Self::Option(Box::new(s.lub(t))), (Self::List(s), Self::List(t)) => Self::List(Box::new(s.lub(t))), @@ -575,10 +527,7 @@ impl StaticType { } pub fn is_number(&self) -> bool { - matches!( - self, - Self::Number | Self::Float | Self::Int | Self::Rational | Self::Complex - ) + matches!(self, Self::Number | Self::Float | Self::Int) } /// Checks whether some runtime value could satisfy both `self` and `other`. @@ -751,8 +700,6 @@ impl StaticType { | Self::Number | Self::Float | Self::Int - | Self::Rational - | Self::Complex | Self::Map { .. } | Self::Struct { .. } // for now, we won't have positional unpacking | Self::Never => None, @@ -781,8 +728,6 @@ impl fmt::Display for StaticType { Self::Number => write!(f, "Number"), Self::Float => write!(f, "Float"), Self::Int => write!(f, "Int"), - Self::Rational => write!(f, "Rational"), - Self::Complex => write!(f, "Complex"), Self::Sequence(elem) => write!(f, "Sequence<{elem}>"), Self::List(elem) => write!(f, "List<{elem}>"), Self::String => write!(f, "String"), @@ -887,4 +832,14 @@ mod test { ); } } + + #[test] + fn public_numeric_types_are_siblings() { + assert!(!StaticType::Int.is_subtype(&StaticType::Number)); + assert!(!StaticType::Float.is_subtype(&StaticType::Number)); + assert_eq!(StaticType::Int.lub(&StaticType::Float), StaticType::Any); + assert_eq!(StaticType::Int.lub(&StaticType::Number), StaticType::Any); + assert!(StaticType::from_name_and_args("Rational", vec![]).is_err()); + assert!(StaticType::from_name_and_args("Complex", vec![]).is_err()); + } } diff --git a/ndc_lexer/src/lib.rs b/ndc_lexer/src/lib.rs index 207d983d..1ba01226 100644 --- a/ndc_lexer/src/lib.rs +++ b/ndc_lexer/src/lib.rs @@ -11,7 +11,7 @@ use string::StringLexer; pub use source_db::SourceDb; pub use span::{SourceId, Span}; -pub use token::{Token, TokenLocation}; +pub use token::{NumericLiteral, Token, TokenLocation}; pub struct Lexer<'a> { source: SourceIterator<'a>, diff --git a/ndc_lexer/src/number.rs b/ndc_lexer/src/number.rs index eb3fc994..538df38b 100644 --- a/ndc_lexer/src/number.rs +++ b/ndc_lexer/src/number.rs @@ -1,7 +1,7 @@ use num::{BigInt, Complex, Num}; use super::Error; -use super::{Lexer, Token, TokenLocation}; +use super::{Lexer, NumericLiteral, Token, TokenLocation}; pub trait NumberLexer { fn lex_number(&mut self) -> Result; @@ -9,6 +9,12 @@ pub trait NumberLexer { trait NumberLexerHelper { fn lex_to_buffer(&mut self, buf: &mut String, is_valid: impl Fn(char) -> bool); + fn lex_integer_with_radix( + &mut self, + start_offset: usize, + radix: u32, + allow_number_suffix: bool, + ) -> Result; } impl NumberLexerHelper for Lexer<'_> { @@ -26,6 +32,62 @@ impl NumberLexerHelper for Lexer<'_> { } } } + + fn lex_integer_with_radix( + &mut self, + start_offset: usize, + radix: u32, + allow_number_suffix: bool, + ) -> Result { + let mut buf = String::new(); + self.lex_to_buffer(&mut buf, |c| c.is_digit(radix)); + + let is_number = if matches!(self.source.peek(), Some('n')) { + if !allow_number_suffix { + return Err(Error::text( + "the `n` suffix is not supported on arbitrary-radix literals".to_string(), + self.source.create_span(start_offset), + )); + } + self.source.next(); + true + } else { + false + }; + + match self.source.peek() { + Some(c) if c.is_ascii_digit() => { + let span = self.source.span(); + self.source.next(); + return Err(Error::text( + format!("invalid digit for base {radix} literal"), + span, + )); + } + Some(c) if c.is_ascii_alphabetic() => { + let span = self.source.span(); + self.source.next(); + return Err(Error::text( + format!("invalid suffix for base {radix} literal"), + span, + )); + } + _ => {} + } + + let span = self.source.create_span(start_offset); + let literal = if is_number { + buf_to_number_literal_with_radix(&buf, radix) + } else { + buf_to_primitive_literal_with_radix(&buf, radix, span)? + } + .ok_or_else(|| Error::text(format!("invalid base {radix} number"), span))?; + + Ok(TokenLocation { + token: Token::NumericLiteral(literal), + span, + }) + } } impl NumberLexer for Lexer<'_> { @@ -37,76 +99,19 @@ impl NumberLexer for Lexer<'_> { let first_char = self .source .next() - .expect("the existance of the first char was guaranteed by the caller"); - - // If the string starts with `0b` it must be a binary literal - if first_char == '0' && matches!(self.source.peek(), Some('b')) { - self.source.next(); // eat the 'b' + .expect("the existence of the first char was guaranteed by the caller"); - self.lex_to_buffer(&mut buf, |c| c == '1' || c == '0'); - - match self.source.peek() { - Some(c) if c.is_ascii_digit() => { - self.source.next(); - return Err(Error::text( - "invalid digit for base 2 literal".to_string(), - self.source.span(), - )); - } - Some(c) if c.is_ascii_alphabetic() => { - self.source.next(); - return Err(Error::text( - "invalid suffix for base 2 literal".to_string(), - self.source.span(), - )); - } - _ => {} - } - - return match buf_to_token_with_radix(&buf, 2) { - Some(token) => Ok(TokenLocation { - token, - span: self.source.create_span(start_offset), - }), - None => Err(Error::text( - "invalid base 2 number".to_string(), - self.source.create_span(start_offset), - )), - }; - } - - if first_char == '0' && matches!(self.source.peek(), Some('x')) { - self.source.next(); - - self.lex_to_buffer(&mut buf, |c| c.is_ascii_hexdigit()); - - return match buf_to_token_with_radix(&buf, 16) { - Some(token) => Ok(TokenLocation { - token, - span: self.source.create_span(start_offset), - }), - None => Err(Error::text( - "invalid base 16 number".to_string(), - self.source.create_span(start_offset), - )), - }; - } - - if first_char == '0' && matches!(self.source.peek(), Some('o')) { - self.source.next(); - - self.lex_to_buffer(&mut buf, |c| matches!(c, '0'..='7')); - - return match buf_to_token_with_radix(&buf, 8) { - Some(token) => Ok(TokenLocation { - token, - span: self.source.create_span(start_offset), - }), - None => Err(Error::text( - "invalid base 16 number".to_string(), - self.source.create_span(start_offset), - )), + if first_char == '0' { + let radix = match self.source.peek() { + Some('b') => Some(2), + Some('o') => Some(8), + Some('x') => Some(16), + _ => None, }; + if let Some(radix) = radix { + self.source.next(); + return self.lex_integer_with_radix(start_offset, radix, true); + } } // The first digit of the literal is not part of a radix but it's part of the number @@ -153,29 +158,39 @@ impl NumberLexer for Lexer<'_> { )); }; - match radix { + return match radix { 2..=36 => { - let mut buf = String::new(); - self.lex_to_buffer(&mut buf, validator_for_radix(usize::from(radix))); - - return match buf_to_token_with_radix(&buf, u32::from(radix)) { - Some(token) => Ok(TokenLocation { - token, - span: self.source.create_span(start_offset), - }), - None => Err(Error::text( - "invalid base 16 number".to_string(), - self.source.create_span(start_offset), - )), - }; + self.lex_integer_with_radix(start_offset, u32::from(radix), false) } - _ => { - return Err(Error::text( - "invalid radix, must be between 2 and 36 OR 64".to_string(), + _ => Err(Error::text( + "invalid radix, must be between 2 and 36 OR 64".to_string(), + self.source.create_span(start_offset), + )), + }; + } + 'n' => { + self.source.next(); + let token = if is_float { + buf.parse::() + .map(NumericLiteral::NumberFloat) + .map_err(|_error| { + Error::text( + format!("invalid Number literal '{buf}n'"), + self.source.create_span(start_offset), + ) + })? + } else { + buf_to_number_literal_with_radix(&buf, 10).ok_or_else(|| { + Error::text( + format!("invalid Number literal '{buf}n'"), self.source.create_span(start_offset), - )); - } - } + ) + })? + }; + return Ok(TokenLocation { + token: Token::NumericLiteral(token), + span: self.source.create_span(start_offset), + }); } 'j' | 'i' => { self.source.next(); @@ -188,7 +203,9 @@ impl NumberLexer for Lexer<'_> { }; return Ok(TokenLocation { - token: Token::Complex(Complex::new(0.0, num)), + token: Token::NumericLiteral(NumericLiteral::Complex(Complex::new( + 0.0, num, + ))), span: self.source.create_span(start_offset), }); } @@ -197,30 +214,134 @@ impl NumberLexer for Lexer<'_> { } } - let Some(token) = buf_to_token_with_radix(&buf, 10) - .or_else(|| buf.parse::().map(Token::Float64).ok()) + let Some(token) = + buf_to_primitive_literal_with_radix(&buf, 10, self.source.create_span(start_offset))? + .or_else(|| buf.parse::().map(NumericLiteral::Float64).ok()) else { // If we've lexed the int/float correctly this error should never happen, that's why it's probably safe to panic panic!("unable to convert buffer into Token"); }; Ok(TokenLocation { - token, + token: Token::NumericLiteral(token), span: self.source.create_span(start_offset), }) } } -fn buf_to_token_with_radix(buf: &str, radix: u32) -> Option { - match i64::from_str_radix(buf, radix) { - Ok(num) => Some(Token::Int64(num)), - Err(_err) => match BigInt::from_str_radix(buf, radix) { - Ok(num) => Some(Token::BigInt(num)), - Err(_err) => None, - }, +fn buf_to_primitive_literal_with_radix( + buf: &str, + radix: u32, + span: crate::Span, +) -> Result, Error> { + if let Ok(num) = i64::from_str_radix(buf, radix) { + return Ok(Some(NumericLiteral::Int64(num))); } + + let Ok(value) = BigInt::from_str_radix(buf, radix) else { + return Ok(None); + }; + + Err(Error::text( + format!("integer literal does not fit in Int; use the advanced literal `{value}n`"), + span, + )) +} + +fn buf_to_number_literal_with_radix(buf: &str, radix: u32) -> Option { + BigInt::from_str_radix(buf, radix) + .ok() + .map(NumericLiteral::NumberInt) } -fn validator_for_radix(radix: usize) -> impl Fn(char) -> bool { - move |c| "0123456789abcdefghijlkmnopqrstuvwxyz"[0..radix].contains(c.to_ascii_lowercase()) +#[cfg(test)] +mod tests { + use super::*; + use crate::SourceId; + + fn lex_one(source: &str) -> Result { + Lexer::new(source, SourceId::SYNTHETIC) + .next() + .expect("literal should produce a lexer result") + } + + #[test] + fn prefixed_integer_literals_share_number_suffix_handling() { + for (source, value) in [ + ("0b101010n", 42.into()), + ("0o52n", 42.into()), + ("0x2an", 42.into()), + ] { + let token = lex_one(source).expect("literal should be valid"); + + assert_eq!( + token.token, + Token::NumericLiteral(NumericLiteral::NumberInt(value)), + "unexpected token for {source}" + ); + assert_eq!(token.span.range(), 0..source.len()); + } + } + + #[test] + fn radix_errors_report_the_actual_base() { + for (source, expected) in [ + ("0b", "invalid base 2 number"), + ("0o", "invalid base 8 number"), + ("0x", "invalid base 16 number"), + ("8r", "invalid base 8 number"), + ] { + let error = lex_one(source).expect_err("literal should be invalid"); + + assert_eq!( + error.to_string(), + expected, + "unexpected diagnostic for {source}" + ); + } + } + + #[test] + fn prefixed_integer_literals_reject_invalid_digits_and_suffixes() { + for (source, expected, expected_range) in [ + ("0b2", "invalid digit for base 2 literal", 2..3), + ("0o8", "invalid digit for base 8 literal", 2..3), + ("0xg", "invalid suffix for base 16 literal", 2..3), + ("0b1n2", "invalid digit for base 2 literal", 4..5), + ("0o7nq", "invalid suffix for base 8 literal", 4..5), + ("0xffnq", "invalid suffix for base 16 literal", 5..6), + ] { + let error = lex_one(source).expect_err("literal should be invalid"); + + assert_eq!( + error.to_string(), + expected, + "unexpected diagnostic for {source}" + ); + assert_eq!( + error.location().range(), + expected_range, + "unexpected diagnostic span for {source}" + ); + } + } + + #[test] + fn oversized_bare_integer_literals_are_rejected_by_the_lexer() { + for literal in [ + "9223372036854775808", + "0b1000000000000000000000000000000000000000000000000000000000000000", + "0o1000000000000000000000", + "0x8000000000000000", + "16r8000000000000000", + ] { + let error = lex_one(literal).expect_err("oversized bare literal should fail lexing"); + + assert_eq!( + error.to_string(), + "integer literal does not fit in Int; use the advanced literal `9223372036854775808n`", + "unexpected diagnostic for {literal}" + ); + } + } } diff --git a/ndc_lexer/src/token.rs b/ndc_lexer/src/token.rs index 9a432614..92ee577e 100644 --- a/ndc_lexer/src/token.rs +++ b/ndc_lexer/src/token.rs @@ -4,14 +4,37 @@ use std::fmt; use super::Span; -#[derive(PartialEq, Clone)] -pub enum Token { - String(String), +#[derive(Debug, PartialEq, Clone)] +pub enum NumericLiteral { Int64(i64), Float64(f64), - BigInt(BigInt), + NumberInt(BigInt), + NumberFloat(f64), Complex(Complex64), - Infinity, +} + +impl fmt::Display for NumericLiteral { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Int64(n) => write!(f, "{n}"), + Self::Float64(n) => { + let mut buffer = ryu::Buffer::new(); + write!(f, "{}", buffer.format(*n)) + } + Self::NumberInt(n) => write!(f, "{n}n"), + Self::NumberFloat(n) => { + let mut buffer = ryu::Buffer::new(); + write!(f, "{}n", buffer.format(*n)) + } + Self::Complex(n) => write!(f, "{n}"), + } + } +} + +#[derive(PartialEq, Clone)] +pub enum Token { + String(String), + NumericLiteral(NumericLiteral), Identifier(String), OpAssign(Box), @@ -98,20 +121,9 @@ impl fmt::Display for Token { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s: &str = match self { Self::String(str) => str, - Self::Int64(n) => { - return write!(f, "{n}"); - } - Self::Float64(n) => { - let mut buffer = ryu::Buffer::new(); - return write!(f, "{}", buffer.format(*n)); - } - Self::BigInt(n) => { - return write!(f, "{n}"); - } - Self::Complex(n) => { + Self::NumericLiteral(n) => { return write!(f, "{n}"); } - Self::Infinity => "Inf", Self::Identifier(ident) => ident, // Self::DeclareVar => ":=", Self::EqualsSign => "=", @@ -309,8 +321,8 @@ impl From for Token { fn from(value: String) -> Self { match value.as_str() { // YOLO for now just have Inf and NaN here - "Inf" => Self::Float64(f64::INFINITY), - "NaN" => Self::Float64(f64::NAN), + "Inf" => Self::NumericLiteral(NumericLiteral::Float64(f64::INFINITY)), + "NaN" => Self::NumericLiteral(NumericLiteral::Float64(f64::NAN)), // Normal keywords "and" => Self::LogicAnd, "as" => Self::As, diff --git a/ndc_lsp/src/features/completion.rs b/ndc_lsp/src/features/completion.rs index b3777ebf..5e3a8d37 100644 --- a/ndc_lsp/src/features/completion.rs +++ b/ndc_lsp/src/features/completion.rs @@ -497,11 +497,15 @@ mod tests { panic!("expected Array response"); }; - // One item per arity; type permutations within an arity fold into the - // common supertype (`Number` subsumes `Int` and `Float`). + // One item per arity; type permutations within an arity fold into a + // union display (`Int`, `Float` and `Number` are sibling types). assert_eq!( parameter_details(&items, "randf"), - vec!["()", "(Number)", "(Number, Number)"] + vec![ + "()", + "(Int | Float | Number)", + "(Int | Number, Int | Number)" + ] ); } @@ -528,7 +532,10 @@ mod tests { panic!("expected Array response"); }; - assert_eq!(parameter_details(&items, "randf"), vec!["()", "(Number)"]); + assert_eq!( + parameter_details(&items, "randf"), + vec!["()", "(Number | Int)"] + ); } #[test] diff --git a/ndc_lsp/src/scope_resolve.rs b/ndc_lsp/src/scope_resolve.rs index 855b83a6..106803be 100644 --- a/ndc_lsp/src/scope_resolve.rs +++ b/ndc_lsp/src/scope_resolve.rs @@ -203,10 +203,7 @@ fn collect(expr: &ExpressionLocation, scope: Span, out: &mut Vec) { Expression::Identifier { .. } | Expression::BoolLiteral(_) | Expression::StringLiteral(_) - | Expression::Int64Literal(_) - | Expression::Float64Literal(_) - | Expression::BigIntLiteral(_) - | Expression::ComplexLiteral(_) + | Expression::NumericLiteral(_) | Expression::Break | Expression::Continue => {} Expression::StructDeclaration { name, .. } => { diff --git a/ndc_lsp/src/visitor.rs b/ndc_lsp/src/visitor.rs index 350f3bd6..cd0cf37b 100644 --- a/ndc_lsp/src/visitor.rs +++ b/ndc_lsp/src/visitor.rs @@ -188,10 +188,7 @@ fn child_expressions(expr: &ExpressionLocation) -> Vec<&ExpressionLocation> { Expression::Identifier { .. } | Expression::BoolLiteral(_) | Expression::StringLiteral(_) - | Expression::Int64Literal(_) - | Expression::Float64Literal(_) - | Expression::BigIntLiteral(_) - | Expression::ComplexLiteral(_) + | Expression::NumericLiteral(_) | Expression::Break | Expression::Continue | Expression::StructDeclaration { .. } => {} @@ -358,10 +355,7 @@ fn walk_expression(visitor: &mut impl AstVisitor, expr: &ExpressionLocation) { Expression::Identifier { .. } | Expression::BoolLiteral(_) | Expression::StringLiteral(_) - | Expression::Int64Literal(_) - | Expression::Float64Literal(_) - | Expression::BigIntLiteral(_) - | Expression::ComplexLiteral(_) + | Expression::NumericLiteral(_) | Expression::Break | Expression::Continue | Expression::StructDeclaration { .. } => {} diff --git a/ndc_macros/src/function.rs b/ndc_macros/src/function.rs index 4bcc3801..6f174047 100644 --- a/ndc_macros/src/function.rs +++ b/ndc_macros/src/function.rs @@ -263,7 +263,7 @@ fn map_type_path(p: &syn::TypePath) -> syn::Result { "Result requires angle bracketed arguments", )), }, - "Number" => Ok(quote! { ndc_core::StaticType::Number }), + "AdvancedNumber" => Ok(quote! { ndc_core::StaticType::Number }), "MapValue" => Ok(quote! { ndc_core::StaticType::Map { key: Box::new(ndc_core::StaticType::Any), diff --git a/ndc_macros/src/types.rs b/ndc_macros/src/types.rs index 0947770e..3753fa2b 100644 --- a/ndc_macros/src/types.rs +++ b/ndc_macros/src/types.rs @@ -11,9 +11,9 @@ /// since the generated code differs based on ownership. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NdcType { - /// Owned `Number` + /// Owned `AdvancedNumber` Number, - /// `&Number` + /// `&AdvancedNumber` NumberRef, /// `f64` F64, @@ -112,7 +112,7 @@ fn classify_owned(ty: &syn::Type) -> Option { let last = segments.last()?; match last.ident.to_string().as_str() { - "Number" => Some(NdcType::Number), + "AdvancedNumber" => Some(NdcType::Number), "f64" => Some(NdcType::F64), "bool" => Some(NdcType::Bool), "i64" => Some(NdcType::I64), diff --git a/ndc_macros/src/vm_convert.rs b/ndc_macros/src/vm_convert.rs index f53bd8fa..4fa23d0e 100644 --- a/ndc_macros/src/vm_convert.rs +++ b/ndc_macros/src/vm_convert.rs @@ -79,18 +79,24 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { let temp = format_ident!("vm_temp{position}"); let result = match ndc_type { - NdcType::Number | NdcType::NumberRef => { + NdcType::Number => { let err = arg_error(position, "number"); - let pass = if ndc_type == NdcType::NumberRef { - quote! { &#temp } - } else { - quote! { #temp } - }; VmInputArg { extract: quote! { let #temp = #raw.to_number().ok_or_else(|| #err)?; }, - pass, + pass: quote! { #temp }, + static_type: quote! { ndc_core::StaticType::Number }, + } + } + + NdcType::NumberRef => { + let err = arg_error(position, "number"); + VmInputArg { + extract: quote! { + let #temp = #raw.as_number().ok_or_else(|| #err)?; + }, + pass: quote! { #temp }, static_type: quote! { ndc_core::StaticType::Number }, } } @@ -99,7 +105,10 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { let err = arg_error(position, "float"); VmInputArg { extract: quote! { - let #temp = #raw.to_f64().ok_or_else(|| #err)?; + let ndc_vm::value::Value::Float(#temp) = #raw else { + return Err(#err); + }; + let #temp = *#temp; }, pass: quote! { #temp }, static_type: quote! { ndc_core::StaticType::Float }, @@ -160,11 +169,14 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { let err = arg_error(position, "int"); VmInputArg { extract: quote! { - let #temp = { - let num = #raw.to_number().ok_or_else(|| #err)?; - usize::try_from(num).map_err(|e| { - ndc_vm::error::VmError::native(format!("arg {}: {}", #position, e)) - })? + let #temp = match #raw { + ndc_vm::value::Value::Int(value) => usize::try_from(*value).map_err(|_| { + ndc_vm::error::VmError::native(format!( + "arg {}: expected a non-negative integer, but the value was negative", + #position, + )) + })?, + _ => return Err(#err), }; }, pass: quote! { #temp }, @@ -176,12 +188,9 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { let err = arg_error(position, "int"); VmInputArg { extract: quote! { - let #temp = { - let num = #raw.to_number().ok_or_else(|| #err)?; - match num { - ndc_core::num::Number::Int(i) => i.to_bigint(), - _ => return Err(#err), - } + let #temp = match #raw { + ndc_vm::value::Value::Int(value) => num::BigInt::from(*value), + _ => return Err(#err), }; }, pass: quote! { &#temp }, @@ -190,36 +199,30 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { } NdcType::BigRationalRef => { - let err = arg_error(position, "rational"); + let err = arg_error(position, "an exact integer or rational Number"); VmInputArg { extract: quote! { let #temp = { - let num = #raw.to_number().ok_or_else(|| #err)?; - match num { - ndc_core::num::Number::Rational(r) => *r, - _ => return Err(#err), - } + let num = #raw.as_number().ok_or_else(|| #err)?; + num.to_rational().ok_or_else(|| #err)? }; }, pass: quote! { &#temp }, - static_type: quote! { ndc_core::StaticType::Rational }, + static_type: quote! { ndc_core::StaticType::Number }, } } NdcType::Complex64 => { - let err = arg_error(position, "complex"); + let err = arg_error(position, "a Number"); VmInputArg { extract: quote! { let #temp = { - let num = #raw.to_number().ok_or_else(|| #err)?; - match num { - ndc_core::num::Number::Complex(c) => c, - _ => return Err(#err), - } + let num = #raw.as_number().ok_or_else(|| #err)?; + num.to_complex() }; }, pass: quote! { #temp }, - static_type: quote! { ndc_core::StaticType::Complex }, + static_type: quote! { ndc_core::StaticType::Number }, } } @@ -520,20 +523,18 @@ fn vm_return_for_classified(ty: &syn::Type) -> Option<(TokenStream, TokenStream) NdcType::BigInt => Some(( quote! { Ok(ndc_vm::value::Value::from_number( - ndc_core::num::Number::Int( - ndc_core::int::Int::BigInt(result).simplified() - ) + ndc_vm::value::AdvancedNumber::Int(result) )) }, - quote! { ndc_core::StaticType::Int }, + quote! { ndc_core::StaticType::Number }, )), NdcType::BigRational => Some(( quote! { Ok(ndc_vm::value::Value::from_number( - ndc_core::num::Number::Rational(Box::new(result)) + ndc_vm::value::AdvancedNumber::rational(result) )) }, - quote! { ndc_core::StaticType::Rational }, + quote! { ndc_core::StaticType::Number }, )), // Input-only types — not valid as return types _ => None, diff --git a/ndc_parser/Cargo.toml b/ndc_parser/Cargo.toml index 7ab23ab4..eaf02ce8 100644 --- a/ndc_parser/Cargo.toml +++ b/ndc_parser/Cargo.toml @@ -6,6 +6,5 @@ version.workspace = true [dependencies] ndc_core.workspace = true ndc_lexer.workspace = true -num.workspace = true derive_more = { workspace = true } -thiserror.workspace = true \ No newline at end of file +thiserror.workspace = true diff --git a/ndc_parser/src/expression.rs b/ndc_parser/src/expression.rs index 41dd20c1..9c3c8670 100644 --- a/ndc_parser/src/expression.rs +++ b/ndc_parser/src/expression.rs @@ -3,9 +3,7 @@ use crate::parser::Error as ParseError; use crate::type_expr::TypeExpr; use ndc_core::r#struct::StructId; use ndc_core::{StaticType, TypeSignature}; -use ndc_lexer::Span; -use num::BigInt; -use num::complex::Complex64; +use ndc_lexer::{NumericLiteral, Span}; use std::sync::atomic::{AtomicU32, Ordering}; /// Unique identity for an AST node. Used as a key in side tables (e.g. the @@ -95,10 +93,7 @@ pub enum Expression { // Literals BoolLiteral(bool), StringLiteral(String), - Int64Literal(i64), - Float64Literal(f64), - BigIntLiteral(BigInt), - ComplexLiteral(Complex64), + NumericLiteral(NumericLiteral), Identifier { name: String, resolved: Binding, diff --git a/ndc_parser/src/parser.rs b/ndc_parser/src/parser.rs index 4356dffe..095cc9ed 100644 --- a/ndc_parser/src/parser.rs +++ b/ndc_parser/src/parser.rs @@ -8,7 +8,7 @@ use crate::expression::{Expression, StructField}; use crate::operator::{BinaryOperator, LogicalOperator, UnaryOperator}; use crate::type_expr::TypeExpr; use ndc_core::{Parameter, StaticType, TypeSignature}; -use ndc_lexer::{Span, Token, TokenLocation}; +use ndc_lexer::{NumericLiteral, Span, Token, TokenLocation}; pub struct Parser { tokens: Vec, @@ -794,7 +794,8 @@ impl Parser { .. } => { *start = Some(Box::new( - Expression::Int64Literal(0).to_location(index_expression.span), + Expression::NumericLiteral(NumericLiteral::Int64(0)) + .to_location(index_expression.span), )); } _ => {} @@ -1067,10 +1068,7 @@ impl Parser { let expression = match token_location.token { Token::False => Expression::BoolLiteral(false), Token::True => Expression::BoolLiteral(true), - Token::Int64(num) => Expression::Int64Literal(num), - Token::Float64(num) => Expression::Float64Literal(num), - Token::BigInt(num) => Expression::BigIntLiteral(num), - Token::Complex(num) => Expression::ComplexLiteral(num), + Token::NumericLiteral(literal) => Expression::NumericLiteral(literal), Token::String(value) => Expression::StringLiteral(value), Token::Identifier(identifier) => Expression::Identifier { name: identifier, diff --git a/ndc_stdlib/src/index.rs b/ndc_stdlib/src/index.rs index 968c4ced..3955578e 100644 --- a/ndc_stdlib/src/index.rs +++ b/ndc_stdlib/src/index.rs @@ -259,18 +259,9 @@ fn extract_vm_offset(index_value: &Value, size: usize) -> Result *i, - Value::Object(obj) => match obj.as_ref() { - Object::BigInt(n) => num::ToPrimitive::to_i64(n) - .ok_or_else(|| VmError::native("index too large for i64"))?, - _ => { - return Err(VmError::native( - "Invalid list index. List indices must be convertible to a signed 64-bit integer.", - )); - } - }, _ => { return Err(VmError::native( - "Invalid list index. List indices must be convertible to a signed 64-bit integer.", + "Invalid list index. List indices must be Int values.", )); } }; diff --git a/ndc_stdlib/src/lib.rs b/ndc_stdlib/src/lib.rs index eb1066a5..ef1f503a 100644 --- a/ndc_stdlib/src/lib.rs +++ b/ndc_stdlib/src/lib.rs @@ -37,7 +37,6 @@ pub fn register(env: &mut FunctionRegistry>) { index::register(env); list::ops::register(env); list::register(env); - math::f64::register(env); math::register(env); #[cfg(feature = "rand")] rand::register(env); diff --git a/ndc_stdlib/src/math.rs b/ndc_stdlib/src/math.rs index e19da491..c4f5fa8e 100644 --- a/ndc_stdlib/src/math.rs +++ b/ndc_stdlib/src/math.rs @@ -1,747 +1,1486 @@ use factorial::Factorial; -use ndc_core::num::{BinaryOperatorError, Number}; -use ndc_macros::export_module; -use ndc_vm::value::{Object, SeqValue, Value}; -use num::ToPrimitive; -use std::ops::{Add, Mul}; +use ndc_core::{FunctionRegistry, StaticType}; +use ndc_vm::error::VmError; +use ndc_vm::value::{AdvancedNumber, BinaryOperatorError}; +use ndc_vm::value::{NativeFunc, NativeFunction, NumericMode, NumericRef, Object, Value}; +use num::complex::Complex64; +use num::{BigInt, BigUint, Integer, ToPrimitive}; +use std::cmp::Ordering; +use std::ops::{Add, Div, Mul, Neg, Not, Rem, Sub}; +use std::rc::Rc; -#[export_module] -mod inner { - use std::ops::Sub; +#[derive(Clone, Copy)] +enum BinaryOperation { + Add, + Sub, + Mul, + Div, + FloorDiv, + Rem, + RemEuclid, + Pow, +} + +impl BinaryOperation { + fn name(self) -> &'static str { + match self { + Self::Add => "+", + Self::Sub => "-", + Self::Mul => "*", + Self::Div => "/", + Self::FloorDiv => "\\", + Self::Rem => "%", + Self::RemEuclid => "%%", + Self::Pow => "^", + } + } + + fn documentation(self) -> &'static str { + match self { + Self::Add => "Adds two numbers.", + Self::Sub => "Subtracts two numbers.", + Self::Mul => "Multiplies two numbers.", + Self::Div => "Divides two numbers.", + Self::FloorDiv => { + "Divides two numbers and rounds the quotient toward negative infinity." + } + Self::Rem => "Returns the remainder paired with truncating division.", + Self::RemEuclid => "Returns the Euclidean remainder.", + Self::Pow => "Raises the left operand to the power of the right operand.", + } + } +} + +pub fn register(env: &mut FunctionRegistry>) { + register_binary_arithmetic(env); + register_unary_arithmetic(env); + register_comparisons(env); + register_bitwise(env); + register_constructors(env); + register_aggregates(env); + register_number_helpers(env); + register_integer_helpers(env); + register_conversions(env); + register_transcendentals(env); +} + +fn declare( + env: &mut FunctionRegistry>, + name: &str, + parameters: Vec, + return_type: StaticType, + documentation: impl Into, + func: impl Fn(&[Value]) -> Result + 'static, +) { + env.declare_global_fn(Rc::new(NativeFunction { + name: name.to_string(), + documentation: Some(documentation.into()), + static_type: StaticType::Function { + parameters: Some(parameters), + return_type: Box::new(return_type), + }, + func: NativeFunc::Simple(Box::new(func)), + })); +} + +/// Declares a native whose operands and result have fixed types. +/// +/// A type name is written once and used twice: as the operand's declared +/// [`StaticType`] and as the [`Value`] variant it must arrive in. A signature +/// therefore cannot drift away from the destructuring that enforces it, and +/// the mismatch error is reported from one place. +/// +/// Each operand is bound by name to a reference to its payload, and the body +/// evaluates to the result's payload — or, with a `Result<_>` result type, to a +/// `Result` carrying it: +/// +/// ```ignore +/// declare_typed!(env, "&", (left: Int, right: Int) -> Int, "…", left & right); +/// declare_typed!(env, "-", (value: Int) -> Result, "…", +/// value.checked_neg().ok_or_else(|| overflowed())); +/// ``` +macro_rules! declare_typed { + ($env:expr, $name:expr, ($($operand:ident: $operand_type:ident),+) -> Result<$result:ident>, + $documentation:expr, $body:expr) => { + declare( + $env, + $name, + vec![$(StaticType::$operand_type),+], + StaticType::$result, + $documentation, + move |args| { + let [$(Value::$operand_type($operand)),+] = args else { + return Err(wrong_operands(&[$(StaticType::$operand_type),+], args)); + }; + $body.map(|payload| wrap_payload!($result, payload)) + }, + ) + }; + ($env:expr, $name:expr, ($($operand:ident: $operand_type:ident),+) -> $result:ident, + $documentation:expr, $body:expr) => { + declare_typed!( + $env, $name, ($($operand: $operand_type),+) -> Result<$result>, $documentation, + Ok::<_, VmError>($body) + ) + }; +} + +/// Wraps a result payload as a [`Value`]. `Number` is not a plain variant +/// wrapper, so it needs its own arm. +macro_rules! wrap_payload { + (Number, $payload:expr) => { + Value::from_number($payload) + }; + ($variant:ident, $payload:expr) => { + Value::$variant($payload) + }; +} + +/// Reports operands that do not match a native's declared signature. +#[cold] +#[inline(never)] +fn wrong_operands(expected: &[StaticType], args: &[Value]) -> VmError { + let list = |types: Vec| types.join(", "); + let expected = list(expected.iter().map(StaticType::to_string).collect()); + let got = list( + args.iter() + .map(|arg| arg.diagnostic_type().to_string()) + .collect(), + ); - use anyhow::Context; - use ndc_core::int::Int; - use ndc_core::num::Number; - use num::{BigInt, BigRational, BigUint, Integer, complex::Complex64}; + VmError::native(format!("expected ({expected}), got ({got})")) +} - /// Returns the sign of a number. - /// - /// For `int`, `float`, and `rational` types, this function returns `-1` if the number is negative, `0` if zero, and `1` if positive. - /// For complex numbers, it returns the number divided by its magnitude (`z / |z|`) if non-zero, or `0` if the number is `0`. - pub fn signum(n: &Number) -> Number { - n.signum() +fn arity(args: &[Value], expected: usize) -> Result<(), VmError> { + if args.len() == expected { + Ok(()) + } else { + Err(VmError::native(format!( + "expected {expected} arguments, got {}", + args.len() + ))) } +} + +fn native_error(error: impl std::fmt::Display) -> VmError { + VmError::native(error.to_string()) +} + +/// Runtime overload candidates are inspected in reverse registration order. +/// Keep homogeneous primitive pairs first in that runtime order: dynamic code +/// usually preserves one numeric representation across repeated operations. +const NUMERIC_PAIRS_BY_DYNAMIC_PRIORITY: [(NumericMode, NumericMode); 9] = [ + (NumericMode::Int, NumericMode::Int), + (NumericMode::Float, NumericMode::Float), + (NumericMode::Number, NumericMode::Number), + (NumericMode::Int, NumericMode::Float), + (NumericMode::Float, NumericMode::Int), + (NumericMode::Int, NumericMode::Number), + (NumericMode::Number, NumericMode::Int), + (NumericMode::Float, NumericMode::Number), + (NumericMode::Number, NumericMode::Float), +]; + +fn register_binary_arithmetic(env: &mut FunctionRegistry>) { + const OPERATIONS: [BinaryOperation; 8] = [ + BinaryOperation::Add, + BinaryOperation::Sub, + BinaryOperation::Mul, + BinaryOperation::Div, + BinaryOperation::FloorDiv, + BinaryOperation::Rem, + BinaryOperation::RemEuclid, + BinaryOperation::Pow, + ]; - /// Returns the real part of a complex number. - pub fn real(c: Complex64) -> f64 { - c.re + for operation in OPERATIONS { + for (left_mode, right_mode) in NUMERIC_PAIRS_BY_DYNAMIC_PRIORITY.into_iter().rev() { + let output_mode = left_mode.promote(right_mode); + let parameters = vec![left_mode.static_type(), right_mode.static_type()]; + let (name, documentation) = (operation.name(), operation.documentation()); + + // Choosing the evaluator per overload here, instead of letting one + // shared closure rediscover the output mode on every call, is what + // lets `1 + 2` read two `i64` slots and add them. + match output_mode { + NumericMode::Int => declare( + env, + name, + parameters, + StaticType::Int, + documentation, + move |args| { + let [left, right] = args else { + return Err(wrong_arity(args)); + }; + eval_int_operands(operation, left, right) + }, + ), + NumericMode::Float => declare( + env, + name, + parameters, + StaticType::Float, + documentation, + move |args| { + let [left, right] = args else { + return Err(wrong_arity(args)); + }; + eval_float_operands(operation, left_mode, right_mode, left, right) + }, + ), + NumericMode::Number => declare( + env, + name, + parameters, + StaticType::Number, + documentation, + move |args| { + let [left, right] = args else { + return Err(wrong_arity(args)); + }; + eval_number_operands(operation, left_mode, right_mode, left, right) + }, + ), + } + } } +} - /// Returns the imaginary part of a complex number. - pub fn imag(c: Complex64) -> f64 { - c.im +fn eval_binary( + operation: BinaryOperation, + left_mode: NumericMode, + right_mode: NumericMode, + output_mode: NumericMode, + left: &Value, + right: &Value, +) -> Result { + match output_mode { + NumericMode::Int => eval_int_operands(operation, left, right), + NumericMode::Float => eval_float_operands(operation, left_mode, right_mode, left, right), + NumericMode::Number => eval_number_operands(operation, left_mode, right_mode, left, right), } +} + +/// `promote` only answers `Int` for two `Int` operands, so both are plain +/// `i64` slots and need no numeric-mode round trip. +#[inline] +fn eval_int_operands( + operation: BinaryOperation, + left: &Value, + right: &Value, +) -> Result { + let Value::Int(left_int) = left else { + return Err(wrong_mode(NumericMode::Int, left)); + }; + let Value::Int(right_int) = right else { + return Err(wrong_mode(NumericMode::Int, right)); + }; + + eval_int_binary(operation, *left_int, *right_int).map(Value::Int) +} + +#[inline] +fn eval_float_operands( + operation: BinaryOperation, + left_mode: NumericMode, + right_mode: NumericMode, + left: &Value, + right: &Value, +) -> Result { + let Some(left_float) = primitive_float(left_mode, left) else { + return Err(wrong_mode(left_mode, left)); + }; + let Some(right_float) = primitive_float(right_mode, right) else { + return Err(wrong_mode(right_mode, right)); + }; + + Ok(Value::Float(eval_float_binary( + operation, + left_float, + right_float, + ))) +} - /// Returns the numerator of a rational number. - pub fn numerator(r: &BigRational) -> BigInt { - r.numer().clone() +fn eval_number_operands( + operation: BinaryOperation, + left_mode: NumericMode, + right_mode: NumericMode, + left: &Value, + right: &Value, +) -> Result { + let left = numeric_ref(left_mode, left)?.to_advanced_number(); + let right = numeric_ref(right_mode, right)?.to_advanced_number(); + + eval_number_binary(operation, left, right) + .map(Value::from_number) + .map_err(native_error) +} + +/// Reads an operand of a `Float` overload: an `Int` widens and a `Float` +/// passes through. A `Number` operand never reaches one, because any `Number` +/// promotes the whole operation to `Number`. +#[inline] +fn primitive_float(mode: NumericMode, value: &Value) -> Option { + match (mode, value) { + (NumericMode::Int, Value::Int(value)) => Some(*value as f64), + (NumericMode::Float, Value::Float(value)) => Some(*value), + _ => None, } +} - /// Returns the denominator of a rational number. - pub fn denominator(r: &BigRational) -> BigInt { - r.denom().clone() +/// Kept out of line so the operand destructuring above stays inlineable. +#[cold] +#[inline(never)] +fn wrong_arity(args: &[Value]) -> VmError { + VmError::native(format!("expected 2 arguments, got {}", args.len())) +} + +#[inline] +fn numeric_ref(mode: NumericMode, value: &Value) -> Result, VmError> { + match value.numeric_ref() { + Some(number) if number.mode() == mode => Ok(number), + _ => Err(wrong_mode(mode, value)), } +} + +/// Kept out of line so the operand check above stays small enough to inline +/// into each registered overload. +#[cold] +#[inline(never)] +fn wrong_mode(mode: NumericMode, value: &Value) -> VmError { + VmError::native(format!( + "expected {}, got {}", + mode.static_type(), + value.diagnostic_type() + )) +} + +fn eval_int_binary(operation: BinaryOperation, left: i64, right: i64) -> Result { + let failed = || { + VmError::native(format!( + "integer operation overflowed or is undefined: {left} {} {right}", + operation.name() + )) + }; - /// Returns the sum of all elements in a sequence. - pub fn sum(seq: SeqValue) -> anyhow::Result { - if matches!(&seq, Value::Object(o) if matches!(o.as_ref(), Object::String(_))) { - anyhow::bail!("string cannot be summed"); + // Each operation carries its own precondition, so add, subtract and + // multiply test nothing but their own overflow flag. + match operation { + BinaryOperation::Add => left.checked_add(right).ok_or_else(failed), + BinaryOperation::Sub => left.checked_sub(right).ok_or_else(failed), + BinaryOperation::Mul => left.checked_mul(right).ok_or_else(failed), + BinaryOperation::Div => { + nonzero_divisor(right)?; + left.checked_div(right).ok_or_else(failed) + } + BinaryOperation::FloorDiv => { + nonzero_divisor(right)?; + checked_floor_div(left, right).ok_or_else(failed) + } + // Once the divisor is nonzero a remainder always fits: `i64::MIN % -1` + // is 0, even though the quotient it implies overflows. The checked + // variants report that quotient overflow, so wrap instead. + BinaryOperation::Rem => { + nonzero_divisor(right)?; + Ok(left.wrapping_rem(right)) + } + BinaryOperation::RemEuclid => { + nonzero_divisor(right)?; + Ok(left.wrapping_rem_euclid(right)) + } + BinaryOperation::Pow => { + if right < 0 { + return Err(negative_exponent()); + } + u32::try_from(right) + .ok() + .and_then(|right| left.checked_pow(right)) + .ok_or_else(failed) } - seq.try_into_iter() - .ok_or_else(|| anyhow::anyhow!("cannot sum non-sequence"))? - .try_fold(Number::from(0), |acc, val| { - let n = val - .to_number() - .ok_or_else(|| anyhow::anyhow!("cannot sum {}", val.static_type()))?; - acc.add(&n).map_err(|e| anyhow::anyhow!("{e}")) - }) } +} - /// Returns the product of all elements in a sequence. - pub fn product(seq: SeqValue) -> anyhow::Result { - if matches!(&seq, Value::Object(o) if matches!(o.as_ref(), Object::String(_))) { - anyhow::bail!("string cannot be multiplied"); - } - seq.try_into_iter() - .ok_or_else(|| anyhow::anyhow!("cannot multiply non-sequence"))? - .try_fold(Number::from(1), |acc, val| { - let n = val - .to_number() - .ok_or_else(|| anyhow::anyhow!("cannot multiply {}", val.static_type()))?; - acc.mul(&n).map_err(|e| anyhow::anyhow!("{e}")) - }) +/// A zero divisor is reported as such rather than as the overflow the checked +/// operation would otherwise report. +#[inline] +fn nonzero_divisor(divisor: i64) -> Result<(), VmError> { + if divisor == 0 { + return Err(division_by_zero()); } + Ok(()) +} + +#[cold] +#[inline(never)] +fn division_by_zero() -> VmError { + VmError::native("division by zero".to_string()) +} + +#[cold] +#[inline(never)] +fn negative_exponent() -> VmError { + VmError::native("negative integer exponents require Number operands".to_string()) +} - /// Returns the factorial of a non-negative integer. - pub fn factorial(a: &BigInt) -> anyhow::Result { - let num = - BigUint::try_from(a).context("cannot compute the factorial of a negative number")?; +fn checked_floor_div(left: i64, right: i64) -> Option { + let quotient = left.checked_div(right)?; + let remainder = left.checked_rem(right)?; + if remainder != 0 && (left < 0) != (right < 0) { + quotient.checked_sub(1) + } else { + Some(quotient) + } +} - Ok(num.factorial().into()) +fn eval_float_binary(operation: BinaryOperation, left: f64, right: f64) -> f64 { + match operation { + BinaryOperation::Add => left + right, + BinaryOperation::Sub => left - right, + BinaryOperation::Mul => left * right, + BinaryOperation::Div => left / right, + BinaryOperation::FloorDiv => (left / right).floor(), + BinaryOperation::Rem => left % right, + BinaryOperation::RemEuclid => left.rem_euclid(right), + BinaryOperation::Pow => left.powf(right), } +} - /// Returns the greatest common divisor of two integers. - pub fn gcd(a: &BigInt, b: &BigInt) -> BigInt { - a.gcd(b) +fn eval_number_binary( + operation: BinaryOperation, + left: AdvancedNumber, + right: AdvancedNumber, +) -> Result { + match operation { + BinaryOperation::Add => left.add(right), + BinaryOperation::Sub => left.sub(right), + BinaryOperation::Mul => left.mul(right), + BinaryOperation::Div => left.div(right), + BinaryOperation::FloorDiv => left.floor_div(right), + BinaryOperation::Rem => left.rem(right), + BinaryOperation::RemEuclid => left.checked_rem_euclid(&right), + BinaryOperation::Pow => left.pow(right), } +} + +fn register_unary_arithmetic(env: &mut FunctionRegistry>) { + declare_typed!( + env, + "-", + (value: Int) -> Result, + "Negates an integer.", + value + .checked_neg() + .ok_or_else(|| VmError::native("integer negation overflowed".to_string())) + ); + declare_typed!( + env, + "-", + (value: Float) -> Float, + "Negates a floating-point number.", + -value + ); + declare_typed!( + env, + "-", + (value: Number) -> Number, + "Negates an advanced number.", + value.as_ref().clone().neg() + ); +} + +/// Orders two operands, reporting the pair that has no ordering between them. +fn compare_operands(args: &[Value]) -> Result { + let [left, right] = args else { + return Err(wrong_arity(args)); + }; + + left.partial_cmp(right).ok_or_else(|| { + VmError::native(format!( + "cannot compare {} and {}", + left.diagnostic_type(), + right.diagnostic_type() + )) + }) +} - /// Returns the least common multiple of two integers. - pub fn lcm(a: &BigInt, b: &BigInt) -> BigInt { - a.lcm(b) +fn register_comparisons(env: &mut FunctionRegistry>) { + for (name, predicate, docs) in [ + ( + ">", + (|ordering| ordering == Ordering::Greater) as fn(Ordering) -> bool, + "Returns whether the left value is greater than the right.", + ), + ( + ">=", + |ordering| matches!(ordering, Ordering::Greater | Ordering::Equal), + "Returns whether the left value is greater than or equal to the right.", + ), + ( + "<", + |ordering| ordering == Ordering::Less, + "Returns whether the left value is less than the right.", + ), + ( + "<=", + |ordering| matches!(ordering, Ordering::Less | Ordering::Equal), + "Returns whether the left value is less than or equal to the right.", + ), + ] { + declare( + env, + name, + vec![StaticType::Any, StaticType::Any], + StaticType::Bool, + docs, + move |args| Ok(Value::Bool(predicate(compare_operands(args)?))), + ); } - /// Returns the smallest integer greater than or equal to the number. - pub fn ceil(number: &Number) -> Number { - number.ceil() + declare( + env, + "==", + vec![StaticType::Any, StaticType::Any], + StaticType::Bool, + "Returns whether two values are equal.", + |args| { + arity(args, 2)?; + Ok(Value::Bool(args[0] == args[1])) + }, + ); + declare( + env, + "!=", + vec![StaticType::Any, StaticType::Any], + StaticType::Bool, + "Returns whether two values are not equal.", + |args| { + arity(args, 2)?; + Ok(Value::Bool(args[0] != args[1])) + }, + ); + + for (name, reverse, docs) in [ + ("<=>", false, "Performs a three-way comparison."), + (">=<", true, "Performs a reverse three-way comparison."), + ] { + declare( + env, + name, + vec![StaticType::Any, StaticType::Any], + StaticType::Int, + docs, + move |args| { + let result = match compare_operands(args)? { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + }; + Ok(Value::Int(if reverse { -result } else { result })) + }, + ); } +} - /// Rounds the number to the nearest integer, with ties rounding away from zero. - pub fn round(number: &Number) -> Number { - number.round() +fn register_bitwise(env: &mut FunctionRegistry>) { + for (name, operation, docs) in [ + ( + "&", + (|left, right| left & right) as fn(i64, i64) -> i64, + "Computes bitwise AND of two integers.", + ), + ( + "|", + |left, right| left | right, + "Computes bitwise OR of two integers.", + ), + ( + "~", + |left, right| left ^ right, + "Computes bitwise XOR of two integers.", + ), + ] { + declare_typed!( + env, + name, + (left: Int, right: Int) -> Int, + docs, + operation(*left, *right) + ); } - /// Returns the largest integer less than or equal to the number. - pub fn floor(number: &Number) -> Number { - number.floor() + for (name, operation, docs) in [ + ( + "&", + (|left, right| left & right) as fn(bool, bool) -> bool, + "Computes logical AND of two booleans.", + ), + ( + "|", + |left, right| left | right, + "Computes logical OR of two booleans.", + ), + ( + "~", + |left, right| left ^ right, + "Computes logical XOR of two booleans.", + ), + ] { + declare_typed!( + env, + name, + (left: Bool, right: Bool) -> Bool, + docs, + operation(*left, *right) + ); } - /// Returns the absolute value of a number. - pub fn abs(number: &Number) -> Number { - number.abs() + declare_typed!( + env, + "~", + (value: Int) -> Int, + "Computes bitwise NOT of an integer.", + value.not() + ); + + for name in ["!", "not"] { + declare_typed!( + env, + name, + (value: Bool) -> Bool, + "Computes logical negation.", + !value + ); } - /// Returns the absolute difference between two numbers. - pub fn abs_diff(left: &Number, right: &Number) -> Result { - Ok(left.sub(right)?.abs()) + for (name, left_shift) in [("<<", true), (">>", false)] { + declare_typed!( + env, + name, + (left: Int, right: Int) -> Result, + "Shifts an integer by a checked non-negative amount.", + { + let amount = u32::try_from(*right) + .map_err(|_error| VmError::native("invalid shift amount".to_string()))?; + if left_shift { + // `checked_shl` only rejects an out-of-range amount, never + // a result that does not fit, so `1 << 63` would other- + // wise wrap to a negative silently. Verify the shift is + // reversible instead; zero survives any amount. + if *left == 0 { + Ok(0) + } else { + left.checked_shl(amount) + .filter(|shifted| shifted >> amount == *left) + .ok_or_else(|| { + VmError::native("integer shift overflowed".to_string()) + }) + } + } else { + left.checked_shr(amount) + .ok_or_else(|| VmError::native("invalid shift amount".to_string())) + } + } + ); } +} - /// Converts a value to a floating-point number. - pub fn float(value: Value) -> anyhow::Result { - match &value { - Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }), - Value::Object(obj) => match obj.as_ref() { - Object::String(s) => Ok(s.borrow().parse::()?), - _ => value.to_f64().ok_or_else(|| { - anyhow::anyhow!("cannot convert {} to float", value.static_type()) - }), +fn register_constructors(env: &mut FunctionRegistry>) { + for mode in NumericMode::ALL { + declare( + env, + "Number", + vec![mode.static_type()], + StaticType::Number, + "Wraps a primitive numeric value as a Number.", + move |args| { + arity(args, 1)?; + Ok(Value::from_number( + numeric_ref(mode, &args[0])?.to_advanced_number(), + )) }, - _ => value - .to_f64() - .ok_or_else(|| anyhow::anyhow!("cannot convert {} to float", value.static_type())), + ); + } +} + +fn register_aggregates(env: &mut FunctionRegistry>) { + for mode in NumericMode::ALL { + for (name, product) in [("sum", false), ("product", true)] { + declare( + env, + name, + vec![StaticType::Sequence(Box::new(mode.static_type()))], + mode.static_type(), + if product { + "Returns the product of a numeric sequence." + } else { + "Returns the sum of a numeric sequence." + }, + move |args| aggregate(args, mode, product), + ); } } +} - /// Computes the four-quadrant arctangent of `y` and `x` in radians. - pub fn atan2(y: f64, x: f64) -> f64 { - y.atan2(x) - } - - /// Converts a given `Value` to an `Int`. - /// - /// Conversion rules: - /// - Integers are unchanged - /// - Floating-point numbers have their decimal part truncated - /// - Rational numbers are rounded down - /// - `true` is converted to `1`, and `false` to `0` - /// - Strings are parsed as decimal integers; other representations result in an error - pub fn int(value: Value) -> anyhow::Result { - match &value { - Value::Bool(b) => Ok(Number::from(if *b { 1i32 } else { 0i32 })), - Value::Object(obj) => match obj.as_ref() { - Object::String(s) => { - let bi = s.borrow().parse::()?; - Ok(Number::Int(Int::BigInt(bi).simplified())) - } - _ => value - .to_number() - .ok_or_else(|| { - anyhow::anyhow!("cannot convert {} to int", value.static_type()) - })? - .to_int_lossy() - .map_err(|e| anyhow::anyhow!("{e}")), - }, - _ => value - .to_number() - .ok_or_else(|| anyhow::anyhow!("cannot convert {} to int", value.static_type()))? - .to_int_lossy() - .map_err(|e| anyhow::anyhow!("{e}")), +fn aggregate(args: &[Value], mode: NumericMode, product: bool) -> Result { + arity(args, 1)?; + + if let Value::Object(object) = &args[0] { + match object.as_ref() { + Object::List(values) => { + let values = values.borrow(); + return aggregate_values(values.iter(), mode, product); + } + Object::Tuple(values) => return aggregate_values(values.iter(), mode, product), + Object::Deque(values) => { + let values = values.borrow(); + return aggregate_values(values.iter(), mode, product); + } + _ => {} } } + + let values = args[0] + .clone() + .try_into_iter() + .ok_or_else(|| VmError::native("expected a sequence".to_string()))?; + aggregate_values(values, mode, product) } -pub mod f64 { - use super::{Number, ToPrimitive, f64}; - use ndc_core::StaticType; - use ndc_core::int::Int; - use ndc_core::num::BinaryOperatorError; - use ndc_vm::error::VmError; - use ndc_vm::value::{NativeFunc, NativeFunction, Value}; - use std::cmp::Ordering; - use std::ops::Not; - use std::rc::Rc; - - pub fn register(env: &mut ndc_core::FunctionRegistry>) { - macro_rules! implement_binary_operator_on_num { - ($operator:literal,$method:expr,$docs:literal) => { - env.declare_global_fn(Rc::new(NativeFunction { - name: $operator.to_string(), - documentation: Some($docs.to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Number, StaticType::Number]), - return_type: Box::new(StaticType::Number), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => { - let l = left.to_number().ok_or_else(|| { - VmError::native(format!( - "expected number, got {}", - left.static_type() - )) - })?; - let r = right.to_number().ok_or_else(|| { - VmError::native(format!( - "expected number, got {}", - right.static_type() - )) - })?; - $method(l, r) - .map(Value::from_number) - .map_err(|e: BinaryOperatorError| VmError::native(e.to_string())) - } - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - }; +fn aggregate_values(mut values: I, mode: NumericMode, product: bool) -> Result +where + I: Iterator, + I::Item: std::borrow::Borrow, +{ + match mode { + NumericMode::Int => { + let initial: i64 = if product { 1 } else { 0 }; + let value = values.try_fold(initial, |accumulator, value| { + let value = std::borrow::Borrow::borrow(&value); + let Value::Int(value) = value else { + return Err(VmError::native("expected a sequence of Int".to_string())); + }; + if product { + accumulator.checked_mul(*value) + } else { + accumulator.checked_add(*value) + } + .ok_or_else(|| VmError::native("integer aggregate overflowed".to_string())) + })?; + Ok(Value::Int(value)) + } + NumericMode::Float => { + let initial = if product { 1.0 } else { 0.0 }; + let value = values.try_fold(initial, |accumulator, value| { + let value = std::borrow::Borrow::borrow(&value); + let Value::Float(value) = value else { + return Err(VmError::native("expected a sequence of Float".to_string())); + }; + Ok::<_, VmError>(if product { + accumulator * *value + } else { + accumulator + *value + }) + })?; + Ok(Value::Float(value)) } + NumericMode::Number => { + let initial = AdvancedNumber::Int(BigInt::from(if product { 1 } else { 0 })); + let value = values.try_fold(initial, |accumulator, value| { + let value = std::borrow::Borrow::borrow(&value); + let Value::Number(value) = value else { + return Err(VmError::native("expected a sequence of Number".to_string())); + }; + if product { + accumulator.mul(value.as_ref().clone()) + } else { + accumulator.add(value.as_ref().clone()) + } + .map_err(native_error) + })?; + Ok(Value::from_number(value)) + } + } +} - implement_binary_operator_on_num!("-", std::ops::Sub::sub, "Subtracts two numbers."); - implement_binary_operator_on_num!("+", std::ops::Add::add, "Adds two numbers."); - implement_binary_operator_on_num!("*", std::ops::Mul::mul, "Multiplies two numbers."); - implement_binary_operator_on_num!("/", std::ops::Div::div, "Divides two numbers."); - implement_binary_operator_on_num!( - "\\", - Number::floor_div, - "Integer (floor) division of two numbers." - ); - implement_binary_operator_on_num!( - "^", - Number::pow, - "Raises the first number to the power of the second." +#[derive(Clone, Copy)] +enum PreservingUnary { + Signum, + Ceil, + Floor, + Round, + Abs, +} + +fn register_number_helpers(env: &mut FunctionRegistry>) { + // Reversed for the same reason as `register_binary_arithmetic`: runtime + // candidates are inspected in reverse registration order, so registering + // `Int` last puts it first where dynamic dispatch looks. + for mode in NumericMode::ALL.into_iter().rev() { + declare( + env, + "signum", + vec![mode.static_type()], + mode.static_type(), + "Returns -1 if the number is negative, 0 if it is zero, and 1 if it is positive.", + move |args| unary_preserving(args, mode, PreservingUnary::Signum), ); - implement_binary_operator_on_num!( - "%", - std::ops::Rem::rem, - "Returns the remainder of dividing two numbers." + for (name, operation, documentation) in [ + ( + "ceil", + PreservingUnary::Ceil, + "Returns the smallest integer greater than or equal to the number.", + ), + ( + "floor", + PreservingUnary::Floor, + "Returns the largest integer less than or equal to the number.", + ), + ( + "round", + PreservingUnary::Round, + "Rounds the number to the nearest integer, with ties rounding away from zero.", + ), + ( + "abs", + PreservingUnary::Abs, + "Returns the absolute value of a number.", + ), + ] { + declare( + env, + name, + vec![mode.static_type()], + mode.static_type(), + documentation, + move |args| unary_preserving(args, mode, operation), + ); + } + } + + for left in NumericMode::ALL { + for right in NumericMode::ALL { + let output = left.promote(right); + declare( + env, + "abs_diff", + vec![left.static_type(), right.static_type()], + output.static_type(), + "Returns the absolute difference between two numbers.", + move |args| { + arity(args, 2)?; + let difference = eval_binary( + BinaryOperation::Sub, + left, + right, + output, + &args[0], + &args[1], + )?; + unary_preserving(&[difference], output, PreservingUnary::Abs) + }, + ); + } + } + + for (name, imaginary, documentation) in [ + ("real", false, "Returns the real part of a complex number."), + ( + "imag", + true, + "Returns the imaginary part of a complex number.", + ), + ] { + declare_typed!( + env, + name, + (value: Number) -> Number, + documentation, + match (value.as_ref(), imaginary) { + (AdvancedNumber::Complex(value), false) => AdvancedNumber::Float(value.re), + (AdvancedNumber::Complex(value), true) => AdvancedNumber::Float(value.im), + (_, false) => value.as_ref().clone(), + (_, true) => AdvancedNumber::Int(BigInt::from(0)), + } ); - implement_binary_operator_on_num!( - "%%", - Number::checked_rem_euclid, - "Returns the Euclidean remainder of dividing two numbers. The result is always non-negative." + } + + for (name, numerator, documentation) in [ + ( + "numerator", + true, + "Returns the numerator of a rational number.", + ), + ( + "denominator", + false, + "Returns the denominator of a rational number.", + ), + ] { + declare_typed!( + env, + name, + (value: Number) -> Number, + documentation, + match value.as_ref() { + AdvancedNumber::Int(value) if numerator => AdvancedNumber::Int(value.clone()), + AdvancedNumber::Int(_) => AdvancedNumber::Int(BigInt::from(1)), + AdvancedNumber::Rational(value) if numerator => { + AdvancedNumber::Int(value.numer().clone()) + } + AdvancedNumber::Rational(value) => AdvancedNumber::Int(value.denom().clone()), + _ => { + return Err(VmError::native( + "expected an exact integer or rational Number".to_string(), + )); + } + } ); + } +} - // Int-specific overloads: fast path on i64, fall back to Number on overflow/BigInt. - macro_rules! implement_binary_operator_on_int { - ($operator:literal, $checked_method:ident, $fallback:expr, $docs:literal) => { - env.declare_global_fn(Rc::new(NativeFunction { - name: $operator.to_string(), - documentation: Some($docs.to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Int]), - return_type: Box::new(StaticType::Int), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [Value::Int(l), Value::Int(r)] => { - if let Some(result) = l.$checked_method(*r) { - Ok(Value::Int(result)) - } else { - let l = Int::Int64(*l); - let r = Int::Int64(*r); - Ok(Value::from_int($fallback(l, r))) - } - } - [left, right] => { - let l = left.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", left.static_type())) - })?; - let r = right.to_int().ok_or_else(|| { - VmError::native(format!( - "expected int, got {}", - right.static_type() - )) - })?; - Ok(Value::from_int($fallback(l, r))) - } - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); +fn unary_preserving( + args: &[Value], + mode: NumericMode, + operation: PreservingUnary, +) -> Result { + arity(args, 1)?; + match (mode, &args[0]) { + (NumericMode::Int, Value::Int(value)) => { + let result = match operation { + PreservingUnary::Signum => value.signum(), + PreservingUnary::Ceil | PreservingUnary::Floor | PreservingUnary::Round => *value, + PreservingUnary::Abs => value.checked_abs().ok_or_else(|| { + VmError::native("integer absolute value overflowed".to_string()) + })?, + }; + Ok(Value::Int(result)) + } + (NumericMode::Float, Value::Float(value)) => { + let result = match operation { + PreservingUnary::Signum => value.signum(), + PreservingUnary::Ceil => value.ceil(), + PreservingUnary::Floor => value.floor(), + PreservingUnary::Round => value.round(), + PreservingUnary::Abs => value.abs(), + }; + Ok(Value::Float(result)) + } + (NumericMode::Number, Value::Number(value)) => { + let result = match operation { + PreservingUnary::Signum => value.signum(), + PreservingUnary::Ceil => value.ceil(), + PreservingUnary::Floor => value.floor(), + PreservingUnary::Round => value.round(), + PreservingUnary::Abs => value.abs(), }; + Ok(Value::from_number(result)) } + _ => Err(VmError::native(format!( + "expected {}, got {}", + mode.static_type(), + args[0].diagnostic_type() + ))), + } +} - implement_binary_operator_on_int!( - "+", - checked_add, - std::ops::Add::add, - "Adds two integers." - ); - implement_binary_operator_on_int!( - "-", - checked_sub, - std::ops::Sub::sub, - "Subtracts two integers." - ); - implement_binary_operator_on_int!( - "*", - checked_mul, - std::ops::Mul::mul, - "Multiplies two integers." +fn register_integer_helpers(env: &mut FunctionRegistry>) { + declare_typed!( + env, + "factorial", + (value: Int) -> Result, + "Returns the checked factorial of a non-negative Int.", + { + if *value < 0 { + return Err(VmError::native( + "cannot compute the factorial of a negative number".to_string(), + )); + } + (1..=*value).try_fold(1i64, i64::checked_mul).ok_or_else(|| { + VmError::native("integer factorial overflowed; use a Number argument".to_string()) + }) + } + ); + declare( + env, + "factorial", + vec![StaticType::Number], + StaticType::Number, + "Returns the arbitrary-precision factorial of an exact non-negative Number.", + |args| { + let value = exact_number_integer(args)?; + let value = BigUint::try_from(value).map_err(|_error| { + VmError::native("cannot compute the factorial of a negative number".to_string()) + })?; + Ok(bigint_number(value.factorial().into())) + }, + ); + + for (name, operation, documentation) in [ + ( + "gcd", + (|left: &BigInt, right: &BigInt| left.gcd(right)) as fn(&BigInt, &BigInt) -> BigInt, + "Returns the greatest common divisor of two integers.", + ), + ( + "lcm", + |left: &BigInt, right: &BigInt| left.lcm(right), + "Returns the least common multiple of two integers.", + ), + ] { + declare_typed!( + env, + name, + (left: Int, right: Int) -> Result, + documentation, + operation(&BigInt::from(*left), &BigInt::from(*right)) + .to_i64() + .ok_or_else(|| VmError::native("integer result overflowed".to_string())) ); - // Integer remainder needs an explicit division-by-zero guard: the - // generic fast-path fallback (`Int % Int`) panics on a zero divisor. - env.declare_global_fn(Rc::new(NativeFunction { - name: "%".to_string(), - documentation: Some("Returns the remainder of dividing two integers.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Int]), - return_type: Box::new(StaticType::Int), + declare( + env, + name, + vec![StaticType::Number, StaticType::Number], + StaticType::Number, + "Computes an arbitrary-precision exact integer divisor operation.", + move |args| { + arity(args, 2)?; + let left = exact_integer(&args[0])?; + let right = exact_integer(&args[1])?; + Ok(bigint_number(operation(&left, &right))) }, - func: NativeFunc::Simple(Box::new(|args| match args { - [Value::Int(l), Value::Int(r)] => { - if *r == 0 { - return Err(VmError::native("division by zero".to_string())); - } - if let Some(result) = l.checked_rem(*r) { - Ok(Value::Int(result)) + ); + } +} + +fn exact_number_integer(args: &[Value]) -> Result { + arity(args, 1)?; + exact_integer(&args[0]) +} + +fn exact_integer(value: &Value) -> Result { + let Value::Number(value) = value else { + return Err(VmError::native( + "expected an exact integer Number".to_string(), + )); + }; + match value.as_ref() { + AdvancedNumber::Int(value) => Ok(value.clone()), + AdvancedNumber::Rational(value) if value.is_integer() => Ok(value.to_integer()), + _ => Err(VmError::native( + "expected an exact integer Number".to_string(), + )), + } +} + +fn bigint_number(value: BigInt) -> Value { + Value::from_number(AdvancedNumber::Int(value)) +} + +fn register_conversions(env: &mut FunctionRegistry>) { + declare( + env, + "int", + vec![StaticType::Any], + StaticType::Int, + "Converts a value to a checked i64 Int.", + |args| { + arity(args, 1)?; + convert_to_int(&args[0]).map(Value::Int) + }, + ); + declare( + env, + "float", + vec![StaticType::Any], + StaticType::Float, + "Converts a value to a Float.", + |args| { + arity(args, 1)?; + convert_to_float(&args[0]).map(Value::Float) + }, + ); + + for left_mode in NumericMode::ALL { + for right_mode in NumericMode::ALL { + let output = left_mode.promote(right_mode); + let output = if output == NumericMode::Int { + NumericMode::Float + } else { + output + }; + declare( + env, + "atan2", + vec![left_mode.static_type(), right_mode.static_type()], + output.static_type(), + "Computes the four-quadrant arctangent of y and x.", + move |args| { + arity(args, 2)?; + let left = numeric_ref(left_mode, &args[0])?; + let right = numeric_ref(right_mode, &args[1])?; + if output == NumericMode::Number { + let left = left.to_f64().ok_or_else(|| { + VmError::native("atan2 requires real Number operands".to_string()) + })?; + let right = right.to_f64().ok_or_else(|| { + VmError::native("atan2 requires real Number operands".to_string()) + })?; + Ok(Value::from_number(AdvancedNumber::Float(left.atan2(right)))) } else { - // The fast path only overflows for `i64::MIN % -1`, - // whose mathematical result is 0; fall back to Int. - Ok(Value::from_int(Int::Int64(*l) % Int::Int64(*r))) - } - } - [left, right] => { - let l = left.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", left.static_type())) - })?; - let r = right.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", right.static_type())) - })?; - if r.is_zero() { - return Err(VmError::native("division by zero".to_string())); + let left = left + .to_primitive_float() + .expect("Float atan2 only combines primitive operands"); + let right = right + .to_primitive_float() + .expect("Float atan2 only combines primitive operands"); + Ok(Value::Float(left.atan2(right))) } - Ok(Value::from_int(l % r)) - } - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - - // Float-specific overloads: operate directly on f64. - macro_rules! implement_binary_operator_on_float { - ($operator:literal, $op:expr, $docs:literal) => { - env.declare_global_fn(Rc::new(NativeFunction { - name: $operator.to_string(), - documentation: Some($docs.to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Float, StaticType::Float]), - return_type: Box::new(StaticType::Float), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [Value::Float(l), Value::Float(r)] => Ok(Value::Float($op(*l, *r))), - _ => Err(VmError::native(format!( - "expected 2 float arguments, got {}", - args.len() - ))), - })), - })); - }; + }, + ); } + } +} - implement_binary_operator_on_float!("+", std::ops::Add::add, "Adds two floats."); - implement_binary_operator_on_float!("-", std::ops::Sub::sub, "Subtracts two floats."); - implement_binary_operator_on_float!("*", std::ops::Mul::mul, "Multiplies two floats."); - implement_binary_operator_on_float!("/", std::ops::Div::div, "Divides two floats."); - implement_binary_operator_on_float!( - "%", - std::ops::Rem::rem, - "Returns the remainder of dividing two floats." - ); +fn convert_to_int(value: &Value) -> Result { + // Describing the value is only needed on the error paths, and for a + // container that description walks the whole structure. + let cannot_convert = + || VmError::native(format!("cannot convert {} to Int", value.diagnostic_type())); + if let Some(number) = value.numeric_ref() { + return number.to_i64_truncating().ok_or_else(cannot_convert); + } - env.declare_global_fn(Rc::new(NativeFunction { - name: "-".to_string(), - documentation: Some("Negates a number.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Number]), - return_type: Box::new(StaticType::Number), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [v] => v - .to_number() - .map(std::ops::Neg::neg) - .map(Value::from_number) - .ok_or_else(|| { - VmError::native(format!("expected number, got {}", v.static_type())) - }), - _ => Err(VmError::native(format!( - "expected 1 argument, got {}", - args.len() - ))), - })), - })); - - macro_rules! impl_cmp { - ($operator:literal,$expected:pat,$docs:literal) => { - env.declare_global_fn(Rc::new(NativeFunction { - name: $operator.to_string(), - documentation: Some($docs.to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Any, StaticType::Any]), - return_type: Box::new(StaticType::Bool), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => match left.partial_cmp(right) { - Some($expected) => Ok(Value::Bool(true)), - Some(_) => Ok(Value::Bool(false)), - None => Err(VmError::native(format!( - "cannot compare {} and {}", - left.static_type(), - right.static_type() - ))), - }, - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - }; + let converted = match value { + Value::Bool(value) => return Ok(if *value { 1 } else { 0 }), + Value::Object(value) => match value.as_ref() { + Object::String(value) => return value.borrow().parse::().map_err(native_error), + _ => None, + }, + Value::None => None, + Value::Int(_) | Value::Float(_) | Value::Number(_) => unreachable!("handled above"), + }; + converted.ok_or_else(cannot_convert) +} + +fn convert_to_float(value: &Value) -> Result { + if let Some(number) = value.numeric_ref() { + return number.to_f64().ok_or_else(|| { + VmError::native("cannot convert a complex Number to Float".to_string()) + }); + } + + match value { + Value::Bool(value) => Ok(if *value { 1.0 } else { 0.0 }), + Value::Object(value) => match value.as_ref() { + Object::String(value) => value.borrow().parse::().map_err(native_error), + _ => Err(VmError::native("cannot convert value to Float".to_string())), + }, + Value::None => Err(VmError::native("cannot convert None to Float".to_string())), + Value::Int(_) | Value::Float(_) | Value::Number(_) => unreachable!("handled above"), + } +} + +#[derive(Clone, Copy)] +enum Transcendental { + Acos, + Acosh, + Asin, + Asinh, + Atan, + Atanh, + Cbrt, + Cos, + Exp, + Ln, + Log2, + Log10, + Sin, + Sqrt, + Tan, + Tanh, +} + +impl Transcendental { + const ALL: [Self; 16] = [ + Self::Acos, + Self::Acosh, + Self::Asin, + Self::Asinh, + Self::Atan, + Self::Atanh, + Self::Cbrt, + Self::Cos, + Self::Exp, + Self::Ln, + Self::Log2, + Self::Log10, + Self::Sin, + Self::Sqrt, + Self::Tan, + Self::Tanh, + ]; + + fn name(self) -> &'static str { + match self { + Self::Acos => "acos", + Self::Acosh => "acosh", + Self::Asin => "asin", + Self::Asinh => "asinh", + Self::Atan => "atan", + Self::Atanh => "atanh", + Self::Cbrt => "cbrt", + Self::Cos => "cos", + Self::Exp => "exp", + Self::Ln => "ln", + Self::Log2 => "log2", + Self::Log10 => "log10", + Self::Sin => "sin", + Self::Sqrt => "sqrt", + Self::Tan => "tan", + Self::Tanh => "tanh", } + } - impl_cmp!( - ">", - Ordering::Greater, - "Returns true if the left value is greater than the right." - ); - impl_cmp!( - ">=", - Ordering::Greater | Ordering::Equal, - "Returns true if the left value is greater than or equal to the right." - ); - impl_cmp!( - "<", - Ordering::Less, - "Returns true if the left value is less than the right." - ); - impl_cmp!( - "<=", - Ordering::Less | Ordering::Equal, - "Returns true if the left value is less than or equal to the right." - ); + fn description(self) -> &'static str { + match self { + Self::Acos => "Computes the inverse cosine in radians.", + Self::Acosh => "Computes the inverse hyperbolic cosine.", + Self::Asin => "Computes the inverse sine in radians.", + Self::Asinh => "Computes the inverse hyperbolic sine.", + Self::Atan => "Computes the inverse tangent in radians.", + Self::Atanh => "Computes the inverse hyperbolic tangent.", + Self::Cbrt => "Returns the cube root of the input.", + Self::Cos => "Computes the cosine of an angle in radians.", + Self::Exp => "Raises e to the power of the input.", + Self::Ln => "Returns the natural logarithm of the input.", + Self::Log2 => "Returns the base-2 logarithm of the input.", + Self::Log10 => "Returns the base-10 logarithm of the input.", + Self::Sin => "Computes the sine of an angle in radians.", + Self::Sqrt => "Returns the square root of the input.", + Self::Tan => "Computes the tangent of an angle in radians.", + Self::Tanh => "Computes the hyperbolic tangent.", + } + } - env.declare_global_fn(Rc::new(NativeFunction { - name: "==".to_string(), - documentation: Some("Returns true if two values are equal.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Any, StaticType::Any]), - return_type: Box::new(StaticType::Bool), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => Ok(Value::Bool(left == right)), - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - - env.declare_global_fn(Rc::new(NativeFunction { - name: "!=".to_string(), - documentation: Some("Returns true if two values are not equal.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Any, StaticType::Any]), - return_type: Box::new(StaticType::Bool), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => Ok(Value::Bool(left != right)), - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - - env.declare_global_fn(Rc::new(NativeFunction { - name: "<=>".to_string(), - documentation: Some("Three-way comparison (spaceship operator). Returns -1 if left < right, 0 if equal, 1 if left > right.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Any, StaticType::Any]), - return_type: Box::new(StaticType::Int), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => match left.partial_cmp(right) { - Some(Ordering::Equal) => Ok(Value::Int(0)), - Some(Ordering::Less) => Ok(Value::Int(-1)), - Some(Ordering::Greater) => Ok(Value::Int(1)), - None => Err(VmError::native(format!( - "cannot compare {} and {}", - left.static_type(), - right.static_type() - ))), - }, - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - - env.declare_global_fn(Rc::new(NativeFunction { - name: ">=<".to_string(), - documentation: Some("Reverse three-way comparison. Returns 1 if left < right, 0 if equal, -1 if left > right.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Any, StaticType::Any]), - return_type: Box::new(StaticType::Int), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => match left.partial_cmp(right) { - Some(Ordering::Equal) => Ok(Value::Int(0)), - Some(Ordering::Less) => Ok(Value::Int(1)), - Some(Ordering::Greater) => Ok(Value::Int(-1)), - None => Err(VmError::native(format!( - "cannot compare {} and {}", - left.static_type(), - right.static_type() - ))), - }, - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - - macro_rules! impl_bitop { - ($operator:literal,$operation:expr,$docs_bool:literal,$docs_int:literal) => { - env.declare_global_fn(Rc::new(NativeFunction { - name: $operator.to_string(), - documentation: Some($docs_bool.to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Bool, StaticType::Bool]), - return_type: Box::new(StaticType::Bool), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [Value::Bool(l), Value::Bool(r)] => Ok(Value::Bool($operation(*l, *r))), - _ => Err(VmError::native(format!( - "expected 2 bool arguments, got {}", - args.len() - ))), - })), - })); - env.declare_global_fn(Rc::new(NativeFunction { - name: $operator.to_string(), - documentation: Some($docs_int.to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Int]), - return_type: Box::new(StaticType::Int), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => { - let l = left.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", left.static_type())) - })?; - let r = right.to_int().ok_or_else(|| { - VmError::native(format!( - "expected int, got {}", - right.static_type() - )) - })?; - Ok(Value::from_int($operation(l, r))) - } - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - }; + fn real_domain_description(self) -> &'static str { + match self { + Self::Acos | Self::Asin => " Real inputs outside [-1, 1] produce NaN.", + Self::Acosh => " Real inputs below 1 produce NaN.", + Self::Atanh => " Real inputs with an absolute value greater than 1 produce NaN.", + Self::Ln | Self::Log2 | Self::Log10 | Self::Sqrt => { + " Negative real inputs produce NaN." + } + Self::Asinh + | Self::Atan + | Self::Cbrt + | Self::Cos + | Self::Exp + | Self::Sin + | Self::Tan + | Self::Tanh => "", } + } - impl_bitop!( - "&", - std::ops::BitAnd::bitand, - "Logical AND of two booleans.", - "Bitwise AND of two integers." - ); - impl_bitop!( - "|", - std::ops::BitOr::bitor, - "Logical OR of two booleans.", - "Bitwise OR of two integers." - ); - impl_bitop!( - "~", - std::ops::BitXor::bitxor, - "Logical XOR of two booleans.", - "Bitwise XOR of two integers." - ); + fn documentation(self, mode: NumericMode) -> String { + let mode_description = match mode { + NumericMode::Int => " Converts Int input to Float before evaluation and returns Float.", + NumericMode::Float => " Returns Float for Float input.", + NumericMode::Number => { + " Returns Number for real or complex Number input. Values outside the real domain continue into the complex plane." + } + }; + let domain = if mode == NumericMode::Number { + "" + } else { + self.real_domain_description() + }; - env.declare_global_fn(Rc::new(NativeFunction { - name: "~".to_string(), - documentation: Some("Bitwise NOT of a number.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Number]), - return_type: Box::new(StaticType::Number), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [v] => v - .to_number() - .map(Not::not) - .map(Value::from_number) - .ok_or_else(|| { - VmError::native(format!("expected number, got {}", v.static_type())) - }), - _ => Err(VmError::native(format!( - "expected 1 argument, got {}", - args.len() - ))), - })), - })); - - for ident in ["!", "not"] { - env.declare_global_fn(Rc::new(NativeFunction { - name: ident.to_string(), - documentation: Some( - "Logical negation. Returns the opposite boolean value.".to_string(), - ), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Bool]), - return_type: Box::new(StaticType::Bool), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [Value::Bool(b)] => Ok(Value::Bool(b.not())), - _ => Err(VmError::native(format!( - "expected 1 bool argument, got {}", - args.len() - ))), - })), - })); + format!("{}{domain}{mode_description}", self.description()) + } + + fn apply_float(self, value: f64) -> f64 { + match self { + Self::Acos => value.acos(), + Self::Acosh => value.acosh(), + Self::Asin => value.asin(), + Self::Asinh => value.asinh(), + Self::Atan => value.atan(), + Self::Atanh => value.atanh(), + Self::Cbrt => value.cbrt(), + Self::Cos => value.cos(), + Self::Exp => value.exp(), + Self::Ln => value.ln(), + Self::Log2 => value.log2(), + Self::Log10 => value.log10(), + Self::Sin => value.sin(), + Self::Sqrt => value.sqrt(), + Self::Tan => value.tan(), + Self::Tanh => value.tanh(), } + } - env.declare_global_fn(Rc::new(NativeFunction { - name: ">>".to_string(), - documentation: Some("Right bit shift.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Int]), - return_type: Box::new(StaticType::Int), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => { - let l = left.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", left.static_type())) - })?; - let r = right.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", right.static_type())) - })?; - l.checked_shr(r).map(Value::from_int).ok_or_else(|| { - VmError::native("cannot apply >> operator to operands".to_string()) - }) - } - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - - env.declare_global_fn(Rc::new(NativeFunction { - name: "<<".to_string(), - documentation: Some("Left bit shift.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Int]), - return_type: Box::new(StaticType::Int), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [left, right] => { - let l = left.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", left.static_type())) - })?; - let r = right.to_int().ok_or_else(|| { - VmError::native(format!("expected int, got {}", right.static_type())) - })?; - l.checked_shl(r).map(Value::from_int).ok_or_else(|| { - VmError::native("cannot apply << operator to operands".to_string()) - }) - } - _ => Err(VmError::native(format!( - "expected 2 arguments, got {}", - args.len() - ))), - })), - })); - - macro_rules! delegate_to_f64 { - ($method:ident,$docs:literal) => { - env.declare_global_fn(Rc::new(NativeFunction { - name: stringify!($method).to_string(), - documentation: Some($docs.to_string()), - static_type: StaticType::Function { - parameters: Some(vec![StaticType::Number]), - return_type: Box::new(StaticType::Number), - }, - func: NativeFunc::Simple(Box::new(|args| match args { - [v] => v - .to_number() - .map(|num| match num { - Number::Int(i) => Number::Float(f64::from(i).$method()), - Number::Float(f) => Number::Float(f.$method()), - Number::Rational(r) => { - Number::Float(r.to_f64().unwrap_or(f64::NAN).$method()) - } - Number::Complex(c) => Number::Complex(c.$method()), - }) - .map(Value::from_number) - .ok_or_else(|| { - VmError::native(format!("expected number, got {}", v.static_type())) - }), - _ => Err(VmError::native(format!( - "expected 1 argument, got {}", - args.len() - ))), - })), - })); - }; + fn apply_complex(self, value: Complex64) -> Complex64 { + match self { + Self::Acos => value.acos(), + Self::Acosh => value.acosh(), + Self::Asin => value.asin(), + Self::Asinh => value.asinh(), + Self::Atan => value.atan(), + Self::Atanh => value.atanh(), + Self::Cbrt => value.powf(1.0 / 3.0), + Self::Cos => value.cos(), + Self::Exp => value.exp(), + Self::Ln => value.ln(), + Self::Log2 => value.ln() / std::f64::consts::LN_2, + Self::Log10 => value.ln() / std::f64::consts::LN_10, + Self::Sin => value.sin(), + Self::Sqrt => value.sqrt(), + Self::Tan => value.tan(), + Self::Tanh => value.tanh(), } + } +} - delegate_to_f64!( - acos, - "Computes the arccosine of a number. Return value is in radians in the range [0, pi] or NaN if the number is outside the range [-1, 1]." +fn register_transcendentals(env: &mut FunctionRegistry>) { + for function in Transcendental::ALL { + declare_typed!( + env, + function.name(), + (value: Int) -> Float, + function.documentation(NumericMode::Int), + function.apply_float(*value as f64) ); - delegate_to_f64!(acosh, "Inverse hyperbolic cosine function."); - delegate_to_f64!( - asin, - "Computes the arcsine of a number. Return value is in radians in the range [-pi/2, pi/2] or NaN if the number is outside the range [-1, 1]." + declare_typed!( + env, + function.name(), + (value: Float) -> Float, + function.documentation(NumericMode::Float), + function.apply_float(*value) ); - delegate_to_f64!(asinh, "Inverse hyperbolic sine function."); - delegate_to_f64!( - atan, - "Computes the arctangent of a number. Return value is in radians in the range [-pi/2, pi/2]." + declare_typed!( + env, + function.name(), + (value: Number) -> Number, + function.documentation(NumericMode::Number), + match value.as_ref() { + AdvancedNumber::Complex(value) => { + AdvancedNumber::Complex(function.apply_complex(*value)) + } + value => { + let input = value.to_f64().expect("non-complex Number is real"); + let result = function.apply_float(input); + if result.is_nan() && !input.is_nan() { + AdvancedNumber::Complex(function.apply_complex(Complex64::new(input, 0.0))) + } else { + AdvancedNumber::Float(result) + } + } + } ); - delegate_to_f64!(atanh, "Inverse hyperbolic tangent function."); - delegate_to_f64!(cbrt, "Returns the cube root of a number."); - delegate_to_f64!(cos, "Computes the cosine of a number (in radians)."); - delegate_to_f64!(exp, "Returns `e^(arg)`, (the exponential function)."); - delegate_to_f64!(ln, "Returns the natural logarithm of the number."); - delegate_to_f64!(log2, "Returns the base 2 logarithm of the number."); - delegate_to_f64!(log10, "Returns the base 10 logarithm of the number."); - delegate_to_f64!(sin, "Computes the sine of a number (in radians)."); - delegate_to_f64!(sqrt, "Returns the square root of a number."); - delegate_to_f64!(tan, "Computes the tangent of a number (in radians)."); - delegate_to_f64!(tanh, "Hyperbolic tangent function."); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_arithmetic_operator_has_a_three_by_three_numeric_matrix() { + let mut registry = FunctionRegistry::default(); + register(&mut registry); + + let kinds = [StaticType::Int, StaticType::Float, StaticType::Number]; + for operator in ["+", "-", "*", "/", "\\", "%", "%%", "^"] { + for left in &kinds { + for right in &kinds { + let expected_return = if matches!(left, StaticType::Number) + || matches!(right, StaticType::Number) + { + StaticType::Number + } else if matches!(left, StaticType::Float) + || matches!(right, StaticType::Float) + { + StaticType::Float + } else { + StaticType::Int + }; + + assert!(registry.iter().any(|function| { + function.name == operator + && function.static_type + == StaticType::Function { + parameters: Some(vec![left.clone(), right.clone()]), + return_type: Box::new(expected_return.clone()), + } + })); + } + } + } + } + + #[test] + fn arithmetic_registration_prefers_homogeneous_dynamic_operands() { + let mut registry = FunctionRegistry::default(); + register(&mut registry); + + for operator in ["+", "-", "*", "/", "\\", "%", "%%", "^"] { + let dynamic_order = registry + .iter() + .filter(|function| function.name == operator) + .filter_map(|function| match &function.static_type { + StaticType::Function { + parameters: Some(parameters), + .. + } if parameters.len() == 2 && parameters.iter().all(StaticType::is_number) => { + Some((parameters[0].clone(), parameters[1].clone())) + } + _ => None, + }) + .rev() + .collect::>(); + let expected = NUMERIC_PAIRS_BY_DYNAMIC_PRIORITY + .iter() + .map(|(left, right)| (left.static_type(), right.static_type())) + .collect::>(); + + assert_eq!(dynamic_order, expected, "unexpected {operator} priority"); + } + } + + #[test] + fn transcendental_overloads_have_function_specific_documentation() { + let mut registry = FunctionRegistry::default(); + register(&mut registry); + + for transcendental in Transcendental::ALL { + for mode in NumericMode::ALL { + let expected_type = StaticType::Function { + parameters: Some(vec![mode.static_type()]), + return_type: Box::new(match mode { + NumericMode::Int | NumericMode::Float => StaticType::Float, + NumericMode::Number => StaticType::Number, + }), + }; + let function = registry + .iter() + .find(|function| { + function.name == transcendental.name() + && function.static_type == expected_type + }) + .unwrap_or_else(|| { + panic!( + "missing {}({}) transcendental overload", + transcendental.name(), + mode.static_type() + ) + }); + + assert_eq!( + function.documentation.as_deref(), + Some(transcendental.documentation(mode).as_str()) + ); + } + } } } diff --git a/ndc_stdlib/src/rand.rs b/ndc_stdlib/src/rand.rs index b0394c90..f6eea107 100644 --- a/ndc_stdlib/src/rand.rs +++ b/ndc_stdlib/src/rand.rs @@ -22,7 +22,7 @@ pub fn random_n( #[export_module] mod inner { use itertools::Itertools; - use ndc_core::num::Number; + use ndc_vm::value::AdvancedNumber; /// Randomly shuffles the elements of the list in place. pub fn shuffle(list: &mut [Value]) { @@ -43,38 +43,101 @@ mod inner { } #[function(name = "randf")] - /// Generate a random number between 0 (inclusive) and 1 (exclusive) + /// Generate a random Float between 0 (inclusive) and 1 (exclusive) pub fn randf_0() -> anyhow::Result { random_n(0.0, 1.0) } #[function(name = "randf")] - /// Generate a random number between 0 (inclusive) and `upper` (exclusive) - pub fn randf_1(upper: &Number) -> anyhow::Result { + /// Generate a random Float between 0 (inclusive) and `upper` (exclusive) + pub fn randf_upper_int(upper: i64) -> anyhow::Result { + random_n(0.0, upper as f64) + } + + #[function(name = "randf")] + /// Generate a random Float between 0 (inclusive) and `upper` (exclusive) + pub fn randf_upper_float(upper: f64) -> anyhow::Result { + random_n(0.0, upper) + } + + #[function(name = "randf")] + /// Generate a random Float between 0 (inclusive) and `upper` (exclusive) + pub fn randf_upper_number(upper: &AdvancedNumber) -> anyhow::Result { random_n(0.0, upper.try_into()?) } #[function(name = "randf")] - /// Generate a random number between `lower` (inclusive) and `upper` (exclusive) - pub fn randf_2(lower: &Number, upper: &Number) -> anyhow::Result { + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_int_int(lower: i64, upper: i64) -> anyhow::Result { + random_n(lower as f64, upper as f64) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_int_float(lower: i64, upper: f64) -> anyhow::Result { + random_n(lower as f64, upper) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_float_int(lower: f64, upper: i64) -> anyhow::Result { + random_n(lower, upper as f64) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_float_float(lower: f64, upper: f64) -> anyhow::Result { + random_n(lower, upper) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_int_number(lower: i64, upper: &AdvancedNumber) -> anyhow::Result { + random_n(lower as f64, upper.try_into()?) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_number_int(lower: &AdvancedNumber, upper: i64) -> anyhow::Result { + random_n(lower.try_into()?, upper as f64) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_float_number(lower: f64, upper: &AdvancedNumber) -> anyhow::Result { + random_n(lower, upper.try_into()?) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_number_float(lower: &AdvancedNumber, upper: f64) -> anyhow::Result { + random_n(lower.try_into()?, upper) + } + + #[function(name = "randf")] + /// Generate a random Float between `lower` (inclusive) and `upper` (exclusive) + pub fn randf_number_number( + lower: &AdvancedNumber, + upper: &AdvancedNumber, + ) -> anyhow::Result { random_n(lower.try_into()?, upper.try_into()?) } #[function(name = "randi")] - /// Generate a random number between 0 (inclusive) and 1 (exclusive) + /// Generate a random Int between 0 (inclusive) and the maximum Int value (exclusive) pub fn randi_0() -> anyhow::Result { random_n(0, i64::MAX) } #[function(name = "randi")] - /// Generate a random number between 0 (inclusive) and `upper` (exclusive) - pub fn randi_1(upper: &Number) -> anyhow::Result { - random_n(0, upper.try_into()?) + /// Generate a random Int between 0 (inclusive) and `upper` (exclusive) + pub fn randi_upper_int(upper: i64) -> anyhow::Result { + random_n(0, upper) } #[function(name = "randi")] - /// Generate a random number between `lower` (inclusive) and `upper` (exclusive) - pub fn randi_2(lower: &Number, upper: &Number) -> anyhow::Result { - random_n(lower.try_into()?, upper.try_into()?) + /// Generate a random Int between `lower` (inclusive) and `upper` (exclusive) + pub fn randi_int_int(lower: i64, upper: i64) -> anyhow::Result { + random_n(lower, upper) } } diff --git a/ndc_stdlib/src/serde.rs b/ndc_stdlib/src/serde.rs index bb054e9d..787042d5 100644 --- a/ndc_stdlib/src/serde.rs +++ b/ndc_stdlib/src/serde.rs @@ -1,6 +1,7 @@ use anyhow::{Context, bail}; use ndc_core::hash_map::HashMap; use ndc_macros::export_module; +use ndc_vm::value::AdvancedNumber; use ndc_vm::value::{Object, Value}; use num::ToPrimitive; use serde_json::{Map, Number, Value as JsonValue, json}; @@ -32,6 +33,7 @@ fn value_to_json( Value::Float(f) if f.is_finite() => Ok(json!(f)), Value::Float(_) if lossy => Ok(JsonValue::Null), Value::Float(f) => bail!("cannot convert non-finite float {f} to JSON"), + Value::Number(number) => advanced_number_to_json(number, lossy), Value::Object(obj) => { // Only these variants have interior mutability through which a // value can contain itself; all other variants are leaves or @@ -58,6 +60,26 @@ fn value_to_json( } } +fn advanced_number_to_json( + number: &AdvancedNumber, + lossy: bool, +) -> Result { + match number { + AdvancedNumber::Int(big_int) => Number::from_str(&big_int.to_string()) + .map(JsonValue::Number) + .context("cannot convert bigint to JSON number"), + AdvancedNumber::Float(f) if f.is_finite() => Ok(json!(f)), + AdvancedNumber::Float(_) if lossy => Ok(JsonValue::Null), + AdvancedNumber::Float(f) => bail!("cannot convert non-finite float {f} to JSON"), + AdvancedNumber::Rational(ratio) if lossy => Ok(json!(ratio.to_f64())), + AdvancedNumber::Rational(_) => { + bail!("cannot convert a rational number to JSON, convert it to a float first") + } + AdvancedNumber::Complex(complex) if lossy => Ok(json!(format!("{complex}"))), + AdvancedNumber::Complex(_) => bail!("cannot convert a complex number to JSON"), + } +} + fn object_to_json( obj: &Rc, lossy: bool, @@ -72,15 +94,6 @@ fn object_to_json( }; match obj.as_ref() { - Object::BigInt(big_int) => Number::from_str(&big_int.to_string()) - .map(JsonValue::Number) - .context("cannot convert bigint to JSON number"), - Object::Rational(ratio) if lossy => Ok(json!(ratio.to_f64())), - Object::Rational(_) => { - bail!("cannot convert a rational number to JSON, convert it to a float first") - } - Object::Complex(complex) if lossy => Ok(json!(format!("{complex}"))), - Object::Complex(_) => bail!("cannot convert a complex number to JSON"), Object::Some(inner) if lossy => value_to_json(inner, lossy, active), Object::Some(_) => bail!("cannot convert an option to JSON, unwrap it first"), Object::Iterator(i) if lossy => { diff --git a/ndc_vm/Cargo.toml b/ndc_vm/Cargo.toml index 0bc05aa4..aac4e83b 100644 --- a/ndc_vm/Cargo.toml +++ b/ndc_vm/Cargo.toml @@ -12,5 +12,5 @@ ndc_core.workspace = true ndc_lexer.workspace = true ndc_parser.workspace = true num.workspace = true -ordered-float.workspace = true +ryu.workspace = true thiserror.workspace = true diff --git a/ndc_vm/src/compiler.rs b/ndc_vm/src/compiler.rs index 8341a122..b31d7a1f 100644 --- a/ndc_vm/src/compiler.rs +++ b/ndc_vm/src/compiler.rs @@ -3,7 +3,7 @@ use crate::value::{CompiledFunction, Function}; use crate::{Object, Value}; use ndc_core::r#struct::StructRegistry; use ndc_core::{StaticType, TypeSignature}; -use ndc_lexer::Span; +use ndc_lexer::{NumericLiteral, Span}; use ndc_parser::{ AugmentedAssignmentPlan, Binding, Candidate, CaptureSource, Expression, ExpressionLocation, ForBody, ForIteration, FunctionParameter, LogicalOperator, Lvalue, ResolvedVar, @@ -212,20 +212,19 @@ impl Compiler { let idx = self.ir.add_constant(Value::string(s)); self.ir.write(OpCode::Constant(idx), span); } - Expression::Int64Literal(i) => { - let idx = self.ir.add_constant(Value::int(i)); - self.ir.write(OpCode::Constant(idx), span); - } - Expression::Float64Literal(f) => { - let idx = self.ir.add_constant(Value::float(f)); - self.ir.write(OpCode::Constant(idx), span); - } - Expression::BigIntLiteral(i) => { - let idx = self.ir.add_constant(Value::bigint(i)); - self.ir.write(OpCode::Constant(idx), span); - } - Expression::ComplexLiteral(c) => { - let idx = self.ir.add_constant(Value::complex(c)); + Expression::NumericLiteral(literal) => { + let value = match literal { + NumericLiteral::Int64(i) => Value::int(i), + NumericLiteral::Float64(f) => Value::float(f), + NumericLiteral::NumberInt(i) => { + Value::number(crate::value::AdvancedNumber::Int(i)) + } + NumericLiteral::NumberFloat(f) => { + Value::number(crate::value::AdvancedNumber::Float(f)) + } + NumericLiteral::Complex(c) => Value::complex(c), + }; + let idx = self.ir.add_constant(value); self.ir.write(OpCode::Constant(idx), span); } Expression::Identifier { name, resolved } => { diff --git a/ndc_vm/src/value/mod.rs b/ndc_vm/src/value/mod.rs index bb44dd62..008c1999 100644 --- a/ndc_vm/src/value/mod.rs +++ b/ndc_vm/src/value/mod.rs @@ -1,16 +1,17 @@ mod function; +mod number; +mod numeric; pub use function::*; +pub use number::{AdvancedNumber, BinaryOperatorError, ExactFraction, NumberToFloatError}; +pub use numeric::{NumericMode, NumericRef}; use crate::iterator::SharedIterator; use ndc_core::StaticType; use ndc_core::compare::FallibleOrd; use ndc_core::hash_map::{DefaultHasher, HashMap}; -use ndc_core::int::Int; -use ndc_core::num::Number; use ndc_core::r#struct::StructInfo; use ndc_parser::ResolvedVar; -use ordered_float::OrderedFloat; use std::cell::RefCell; use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; @@ -54,6 +55,7 @@ fn bounded_element_type<'a>( pub enum Value { Int(i64), Float(f64), + Number(Rc), Bool(bool), None, Object(Rc), @@ -62,9 +64,6 @@ pub enum Value { #[derive(Clone)] pub enum Object { Some(Value), - BigInt(num::BigInt), - Complex(num::Complex), - Rational(num::BigRational), String(Rc>), List(RefCell>), Tuple(Vec), @@ -200,18 +199,21 @@ impl Value { } pub fn bigint(i: num::BigInt) -> Self { - Self::Object(Rc::new(Object::BigInt(i))) + Self::number(AdvancedNumber::Int(i)) } pub fn complex(c: num::complex::Complex64) -> Self { - Self::Object(Rc::new(Object::Complex(c))) + Self::number(AdvancedNumber::Complex(c)) + } + + pub fn number(number: AdvancedNumber) -> Self { + Self::Number(Rc::new(number)) } /// Creates a shallow copy: scalars are copied by value; mutable collection /// types (String, List, Map, Deque, heaps, structs) get new independent /// containers with cloned contents; immutable / identity types (Tuple, - /// Function, Iterator, `BigInt`, Rational, Complex, Some, `OverloadSet`) - /// share the Rc. + /// Function, Iterator, Some, `OverloadSet`) share the Rc. pub fn shallow_clone(&self) -> Self { match self { Self::Object(obj) => match obj.as_ref() { @@ -271,7 +273,7 @@ impl Value { /// Returns a runtime type description whose recursion depth and total /// number of inspected values are bounded. Once either limit is reached, /// the remaining subtree is widened to `Any`. - pub(crate) fn diagnostic_type(&self) -> StaticType { + pub fn diagnostic_type(&self) -> StaticType { let mut budget = DIAGNOSTIC_TYPE_VALUE_BUDGET; self.static_type_with_budget(DIAGNOSTIC_TYPE_MAX_DEPTH, &mut budget) } @@ -285,25 +287,19 @@ impl Value { match self { Self::Int(_) => StaticType::Int, Self::Float(_) => StaticType::Float, + Self::Number(_) => StaticType::Number, Self::Bool(_) => StaticType::Bool, Self::None => StaticType::Option(Box::new(StaticType::Any)), Self::Object(obj) => obj.static_type_with_budget(depth, budget), } } - /// Returns `true` if this value is a numeric type (Int, Float, Rational, or Complex). + /// Returns `true` if this value is an Int, Float, or Number. /// /// Prefer this over `self.static_type().is_number()` in hot paths — this is O(1) /// and never allocates, whereas `static_type()` on containers is O(n). pub fn is_number(&self) -> bool { - match self { - Self::Int(_) | Self::Float(_) => true, - Self::Object(obj) => matches!( - obj.as_ref(), - Object::BigInt(_) | Object::Rational(_) | Object::Complex(_) - ), - _ => false, - } + self.numeric_ref().is_some() } /// Check whether this value satisfies a function parameter type at runtime, @@ -311,18 +307,18 @@ impl Value { /// /// When `param` is a container type whose inner types are all `Any` (e.g. /// `Sequence(Any)`, `Map { Any, Any }`, `List(Any)`), only the outer kind is - /// checked — no element iteration occurs. All other cases fall back to + /// checked — no element iteration occurs. Typed container parameters are + /// rejected rather than scanned; the remaining cases fall back to /// `self.static_type().is_subtype(param)`. pub fn matches_param(&self, param: &StaticType) -> bool { match param { StaticType::Any => true, StaticType::Int => { matches!(self, Self::Int(_)) - || matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::BigInt(_))) } StaticType::Float => matches!(self, Self::Float(_)), StaticType::Bool => matches!(self, Self::Bool(_)), - StaticType::Number => self.is_number(), + StaticType::Number => matches!(self, Self::Number(_)), StaticType::String => { matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::String(_))) } @@ -561,59 +557,27 @@ impl Value { obj.function_prototype() } - /// Convert a numeric VM value to a `ndc_core::Number`. - /// Returns `None` for non-numeric values (Bool, None, String, List, …). - pub fn to_number(&self) -> Option { - vm_value_to_number(self) + /// Clone the payload of a Number value. + pub fn to_number(&self) -> Option { + self.as_number().cloned() } - /// Convert a `ndc_core::Number` to a VM value. - /// `Int64` maps to `Value::Int`; all other variants become `Value::Object`. - pub fn from_number(n: Number) -> Self { - match n { - Number::Int(Int::Int64(i)) => Self::Int(i), - Number::Int(Int::BigInt(b)) => Self::Object(Rc::new(Object::BigInt(b))), - Number::Float(f) => Self::Float(f), - Number::Rational(r) => Self::Object(Rc::new(Object::Rational(*r))), - Number::Complex(c) => Self::Object(Rc::new(Object::Complex(c))), - } + pub fn as_number(&self) -> Option<&AdvancedNumber> { + self.numeric_ref().and_then(NumericRef::as_number) } - /// Extract an integer VM value as a `ndc_core::Int`. - /// Returns `None` for non-integer values. - pub fn to_int(&self) -> Option { + pub fn numeric_ref(&self) -> Option> { match self { - Self::Int(i) => Some(Int::Int64(*i)), - Self::Object(obj) => match obj.as_ref() { - Object::BigInt(b) => Some(Int::BigInt(b.clone())), - _ => None, - }, - _ => None, + Self::Int(value) => Some(NumericRef::Int(*value)), + Self::Float(value) => Some(NumericRef::Float(*value)), + Self::Number(value) => Some(NumericRef::Number(value.as_ref())), + Self::Bool(_) | Self::None | Self::Object(_) => None, } } - /// Convert a `ndc_core::Int` to a VM value. - pub fn from_int(i: Int) -> Self { - match i { - Int::Int64(n) => Self::Int(n), - Int::BigInt(b) => Self::Object(Rc::new(Object::BigInt(b))), - } - } - - /// Convert a numeric VM value to `f64`, coercing integers and rationals. - /// Returns `None` for non-numeric values (Bool, None, String, …). - pub fn to_f64(&self) -> Option { - use num::ToPrimitive; - match self { - Self::Float(f) => Some(*f), - Self::Int(i) => i.to_f64(), - Self::Object(obj) => match obj.as_ref() { - Object::BigInt(b) => b.to_f64(), - Object::Rational(r) => r.to_f64(), - _ => None, - }, - _ => None, - } + /// Wrap an advanced numeric payload as a Number value. + pub fn from_number(n: AdvancedNumber) -> Self { + Self::number(n) } } @@ -693,9 +657,6 @@ impl Object { } StaticType::Option(Box::new(inner.static_type_with_budget(depth - 1, budget))) } - Self::BigInt(_) => StaticType::Int, - Self::Complex(_) => StaticType::Complex, - Self::Rational(_) => StaticType::Rational, Self::String(_) => StaticType::String, Self::List(elements) => { if depth == 0 { @@ -792,6 +753,7 @@ impl fmt::Display for Value { match self { Self::Int(n) => write!(f, "{n}"), Self::Float(n) => write!(f, "{n}"), + Self::Number(n) => write!(f, "{n}"), Self::Bool(b) => write!(f, "{b}"), Self::None => write!(f, "None"), Self::Object(obj) => write!(f, "{obj}"), @@ -803,9 +765,6 @@ impl fmt::Display for Object { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::Some(v) => write!(f, "Some({v})"), - Self::BigInt(n) => write!(f, "{n}"), - Self::Complex(c) => write!(f, "{c}"), - Self::Rational(r) => write!(f, "{r}"), // Strings display without quotes at the top level. Self::String(s) => write!(f, "{}", s.borrow()), Self::List(vs) => { @@ -893,9 +852,6 @@ impl fmt::Debug for Object { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::Some(v) => f.debug_tuple("Some").field(v).finish(), - Self::BigInt(n) => f.debug_tuple("BigInt").field(n).finish(), - Self::Complex(c) => f.debug_tuple("Complex").field(c).finish(), - Self::Rational(r) => f.debug_tuple("Rational").field(r).finish(), // Strings in debug/repr context are shown quoted. Self::String(s) => write!(f, "\"{}\"", s.borrow()), Self::List(vs) => f.debug_tuple("List").field(&vs.borrow()).finish(), @@ -935,62 +891,25 @@ impl fmt::Debug for Object { impl PartialOrd for Value { fn partial_cmp(&self, other: &Self) -> Option { + if let (Some(left), Some(right)) = (self.numeric_ref(), other.numeric_ref()) { + return Some(left.compare(right)); + } + match (self, other) { - // Same-type fast paths - (Self::Int(a), Self::Int(b)) => Some(a.cmp(b)), - (Self::Float(a), Self::Float(b)) => OrderedFloat(*a).partial_cmp(&OrderedFloat(*b)), (Self::Bool(a), Self::Bool(b)) => a.partial_cmp(b), (Self::Object(a), Self::Object(b)) => a.partial_cmp(b), - // Cross-type int/float fast paths (avoid BigInt allocation) - (Self::Float(a), Self::Int(b)) => { - OrderedFloat(*a).partial_cmp(&OrderedFloat(*b as f64)) - } - (Self::Int(a), Self::Float(b)) => { - OrderedFloat(*a as f64).partial_cmp(&OrderedFloat(*b)) - } - // Any numeric cross-type comparison (Int/Float vs BigInt/Rational/Complex etc.) - // delegates to ndc_core::Number which handles all cases the interpreter does. - (a, b) => vm_value_to_number(a)?.partial_cmp(&vm_value_to_number(b)?), - } - } -} - -/// Convert a VM numeric value to a `ndc_core::Number` for cross-type comparison. -/// Returns `None` for non-numeric values (Bool, None, String, List, …). -fn vm_value_to_number(v: &Value) -> Option { - match v { - Value::Int(i) => Some(Number::Int(Int::Int64(*i))), - Value::Float(f) => Some(Number::Float(*f)), - Value::Object(obj) => match obj.as_ref() { - Object::BigInt(b) => Some(Number::Int(Int::BigInt(b.clone()))), - Object::Rational(r) => Some(Number::Rational(Box::new(r.clone()))), - Object::Complex(c) => Some(Number::Complex(*c)), _ => None, - }, - _ => None, - } -} - -/// Convert an `Object` numeric value to a `ndc_core::Number` for cross-type comparison. -fn obj_to_number(obj: &Object) -> Option { - match obj { - Object::BigInt(b) => Some(Number::Int(Int::BigInt(b.clone()))), - Object::Rational(r) => Some(Number::Rational(Box::new(r.clone()))), - Object::Complex(c) => Some(Number::Complex(*c)), - _ => None, + } } } impl PartialOrd for Object { fn partial_cmp(&self, other: &Self) -> Option { match (self, other) { - (Self::BigInt(a), Self::BigInt(b)) => a.partial_cmp(b), - (Self::Rational(a), Self::Rational(b)) => a.partial_cmp(b), (Self::String(a), Self::String(b)) => a.borrow().partial_cmp(&*b.borrow()), (Self::List(a), Self::List(b)) => a.borrow().partial_cmp(&*b.borrow()), (Self::Tuple(a), Self::Tuple(b)) => a.partial_cmp(b), - // Cross-type numeric: BigInt vs Rational, BigInt vs Complex, Rational vs Complex, etc. - (a, b) => obj_to_number(a)?.partial_cmp(&obj_to_number(b)?), + _ => None, } } } @@ -1018,13 +937,17 @@ impl Value { Self::Float(f) => f .partial_cmp(&0.0) .ok_or_else(|| "NaN in comparator result".to_string()), - Self::Object(obj) => match obj.as_ref() { - Object::BigInt(b) => Ok(b.cmp(&num::BigInt::from(0))), - Object::Rational(r) => Ok(r.cmp(&num::BigRational::from(num::BigInt::from(0)))), - _ => Err(format!( - "comparator must return a number, got {}", - self.static_type() - )), + Self::Number(number) => match number.as_ref() { + AdvancedNumber::Int(i) => Ok(i.cmp(&num::BigInt::from(0))), + AdvancedNumber::Float(f) => f + .partial_cmp(&0.0) + .ok_or_else(|| "NaN in comparator result".to_string()), + AdvancedNumber::Rational(r) => Ok(r + .as_ref() + .cmp(&num::BigRational::from(num::BigInt::from(0)))), + AdvancedNumber::Complex(_) => { + Err("comparator must return a real number, got a complex number".to_string()) + } }, _ => Err(format!( "comparator must return a number, got {}", @@ -1036,18 +959,15 @@ impl Value { impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { + if let (Some(left), Some(right)) = (self.numeric_ref(), other.numeric_ref()) { + return left == right; + } + match (self, other) { - (Self::Int(a), Self::Int(b)) => a == b, - (Self::Float(a), Self::Float(b)) => OrderedFloat(*a) == OrderedFloat(*b), (Self::Bool(a), Self::Bool(b)) => a == b, (Self::None, Self::None) => true, (Self::Object(a), Self::Object(b)) => a == b, - // Cross-type numeric equality: delegate to Number, consistent with PartialOrd. - // Covers Int vs Float, Int vs Rational, Int vs BigInt, Float vs Rational, etc. - (a, b) => match (vm_value_to_number(a), vm_value_to_number(b)) { - (Some(a), Some(b)) => a == b, - _ => false, - }, + _ => false, } } } @@ -1056,22 +976,13 @@ impl Eq for Value {} impl Hash for Value { fn hash(&self, state: &mut H) { + if let Some(number) = self.numeric_ref() { + state.write_u8(1); + number.hash(state); + return; + } + match self { - Self::Int(n) => { - state.write_u8(1); - n.hash(state); - } - Self::Float(f) => { - // Normalise whole-number floats to the same hash as their integer equivalent, - // so that Int(1) and Float(1.0) hash identically (consistent with PartialEq). - if let Some(i) = Int::from_f64_if_int(*f) { - state.write_u8(1); - i.hash(state); - } else { - state.write_u8(2); - OrderedFloat(*f).hash(state); - } - } Self::Bool(true) => state.write_u8(3), Self::Bool(false) => state.write_u8(4), Self::None => state.write_u8(5), @@ -1079,6 +990,7 @@ impl Hash for Value { state.write_u8(6); o.hash(state); } + Self::Int(_) | Self::Float(_) | Self::Number(_) => unreachable!("handled above"), } } } @@ -1139,12 +1051,7 @@ impl PartialEq for Object { // address is equivalent to comparing the outer Rc pointers. (Self::MinHeap(a), Self::MinHeap(b)) => std::ptr::eq(a, b), (Self::MaxHeap(a), Self::MaxHeap(b)) => std::ptr::eq(a, b), - // Numeric types: delegate to Number for cross-type equality - // (e.g. BigInt(5) == Rational(5/1), Rational(5/1) == Complex(5+0i)). - (a, b) => match (obj_to_number(a), obj_to_number(b)) { - (Some(a), Some(b)) => a == b, - _ => false, - }, + _ => false, } } } @@ -1158,19 +1065,6 @@ impl Hash for Object { state.write_u8(1); v.hash(state); } - Self::BigInt(n) => { - state.write_u8(2); - n.hash(state); - } - Self::Complex(c) => { - state.write_u8(3); - c.re.to_bits().hash(state); - c.im.to_bits().hash(state); - } - Self::Rational(r) => { - state.write_u8(4); - r.hash(state); - } Self::String(s) => { state.write_u8(5); s.borrow().hash(state); @@ -1265,6 +1159,56 @@ impl Hash for Object { mod tests { use super::*; + fn value_hash(value: &Value) -> u64 { + let mut hasher = DefaultHasher::default(); + value.hash(&mut hasher); + hasher.finish() + } + + #[test] + fn numeric_ref_owns_checked_numeric_conversions() { + let integer = Value::Int(42); + let float = Value::Float(42.75); + let rational = Value::number(AdvancedNumber::rational(num::BigRational::new( + 7.into(), + 2.into(), + ))); + let complex = Value::complex(num::Complex::new(1.0, 2.0)); + + assert_eq!(integer.numeric_ref().unwrap().mode(), NumericMode::Int); + assert_eq!(float.numeric_ref().unwrap().mode(), NumericMode::Float); + assert_eq!(rational.numeric_ref().unwrap().mode(), NumericMode::Number); + assert_eq!(float.numeric_ref().unwrap().to_i64_truncating(), Some(42)); + assert_eq!(rational.numeric_ref().unwrap().to_i64_truncating(), Some(3)); + assert_eq!(rational.numeric_ref().unwrap().to_f64(), Some(3.5)); + assert_eq!(complex.numeric_ref().unwrap().to_f64(), None); + assert!(Value::Bool(true).numeric_ref().is_none()); + } + + #[test] + fn equal_numeric_modes_keep_equal_hashes() { + let values = [ + Value::Int(1), + Value::Float(1.0), + Value::number(AdvancedNumber::Int(1.into())), + Value::number(AdvancedNumber::Float(1.0)), + // the constructor collapses this to an integer rather than + // leaving a whole fraction in the rational variant + Value::number(AdvancedNumber::rational(num::BigRational::from_integer( + 1.into(), + ))), + Value::complex(num::Complex::new(1.0, 0.0)), + ]; + + for left in &values { + for right in &values { + assert_eq!(left, right); + assert_eq!(left.partial_cmp(right), Some(Ordering::Equal)); + assert_eq!(value_hash(left), value_hash(right)); + } + } + } + #[test] fn conformance_memoizes_aliased_containers() { let list = Rc::new(Object::List(RefCell::new(Vec::new()))); @@ -1287,4 +1231,79 @@ mod tests { // Break the reference cycle so the test does not leak its allocations. elements.borrow_mut().clear(); } + + #[test] + fn runtime_parameter_matching_does_not_scan_lists() { + let list = Rc::new(Object::List(RefCell::new(vec![Value::Int(1)]))); + let value = Value::Object(Rc::clone(&list)); + let Object::List(elements) = list.as_ref() else { + unreachable!(); + }; + + // Holding a mutable borrow makes any attempted element scan panic. + // Dynamic dispatch must only inspect the outer container kind. + let _borrow = elements.borrow_mut(); + assert!(value.matches_param(&StaticType::List(Box::new(StaticType::Any)))); + assert!(value.matches_param(&StaticType::Sequence(Box::new(StaticType::Any,)))); + assert!(!value.matches_param(&StaticType::List(Box::new(StaticType::Int,)))); + } + + #[test] + fn same_representation_numbers_compare_without_changing_semantics() { + assert!(Value::int(-1) < Value::int(1)); + assert_eq!(Value::float(-0.0), Value::float(0.0)); + assert_eq!(Value::float(f64::NAN), Value::float(f64::NAN)); + assert!(Value::float(f64::NAN) > Value::float(f64::INFINITY)); + + let one = Value::bigint(num::BigInt::from(1)); + let two = Value::bigint(num::BigInt::from(2)); + assert!(one < two); + } + + #[test] + fn cross_representation_numbers_keep_exact_comparison_semantics() { + let integers = [ + i64::MIN, + i64::MIN + 1, + -9_007_199_254_740_993, + -1, + 0, + 1, + 9_007_199_254_740_993, + i64::MAX, + ]; + let floats = [ + f64::NEG_INFINITY, + i64::MIN as f64, + -9_007_199_254_740_992.0, + -1.5, + -0.0, + 0.5, + 1.0, + 9_007_199_254_740_992.0, + i64::MAX as f64, + f64::INFINITY, + f64::NAN, + ]; + + for integer in integers { + for float in floats { + let fast_integer = Value::int(integer); + let fast_float = Value::float(float); + let canonical_integer = AdvancedNumber::Int(num::BigInt::from(integer)); + let canonical_float = AdvancedNumber::Float(float); + + assert_eq!( + fast_integer.partial_cmp(&fast_float), + canonical_integer.partial_cmp(&canonical_float), + "ordering differs for {integer} and {float}" + ); + assert_eq!( + fast_integer == fast_float, + canonical_integer == canonical_float, + "equality differs for {integer} and {float}" + ); + } + } + } } diff --git a/ndc_vm/src/value/number.rs b/ndc_vm/src/value/number.rs new file mode 100644 index 00000000..8df73269 --- /dev/null +++ b/ndc_vm/src/value/number.rs @@ -0,0 +1,874 @@ +use std::cmp::Ordering; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, Div, Mul, Neg, Rem, Sub}; + +use ndc_core::StaticType; +use num::complex::{Complex64, ComplexFloat}; +use num::{BigInt, BigRational, Complex, FromPrimitive, Signed, ToPrimitive, Zero}; + +#[derive(Debug, Clone)] +pub enum AdvancedNumber { + Int(BigInt), + Float(f64), + /// Never holds a whole number — see [`ExactFraction`], which is why that + /// is not merely a convention. + Rational(ExactFraction), + Complex(Complex64), +} + +/// The payload of [`AdvancedNumber::Rational`], holding a fraction that is +/// never whole: a denominator of one belongs in [`AdvancedNumber::Int`], or +/// consumers that switch on the variant reject a value that is an integer in +/// every other sense. +/// +/// The field is private so that [`AdvancedNumber::rational`] is the only way +/// to build one, which makes that rule hold by construction rather than by +/// everyone remembering it. Reading is unrestricted: it derefs to the +/// fraction it wraps. +#[derive(Debug, Clone)] +pub struct ExactFraction(Box); + +impl ExactFraction { + fn into_inner(self) -> BigRational { + *self.0 + } +} + +impl std::ops::Deref for ExactFraction { + type Target = BigRational; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for ExactFraction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +enum CanonicalScalar { + NegInfinity, + Finite(BigRational), + PosInfinity, + NaN, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +struct CanonicalNumber { + real: CanonicalScalar, + imaginary: CanonicalScalar, +} + +impl CanonicalScalar { + fn from_float(value: f64) -> Self { + if value.is_nan() { + Self::NaN + } else if value == f64::NEG_INFINITY { + Self::NegInfinity + } else if value == f64::INFINITY { + Self::PosInfinity + } else { + Self::Finite( + BigRational::from_float(value) + .expect("finite f64 values have an exact rational representation"), + ) + } + } + + fn zero() -> Self { + Self::Finite(BigRational::from_integer(BigInt::from(0))) + } +} + +impl From for AdvancedNumber { + fn from(value: BigInt) -> Self { + Self::Int(value) + } +} + +impl From for AdvancedNumber { + fn from(value: i32) -> Self { + Self::Int(BigInt::from(value)) + } +} + +impl From for AdvancedNumber { + fn from(value: f64) -> Self { + Self::Float(value) + } +} + +impl From for AdvancedNumber { + fn from(value: BigRational) -> Self { + Self::rational(value) + } +} + +impl From for AdvancedNumber { + fn from(value: Complex64) -> Self { + Self::Complex(value) + } +} + +impl PartialOrd for AdvancedNumber { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.canonical().cmp(&other.canonical())) + } +} + +impl PartialEq for AdvancedNumber { + fn eq(&self, other: &Self) -> bool { + self.canonical() == other.canonical() + } +} + +impl Default for AdvancedNumber { + fn default() -> Self { + Self::Int(BigInt::zero()) + } +} + +/// Tags keeping the three hashing schemes below from colliding with each other. +const HASH_TAG_EXACT_I64: u8 = 0; +const HASH_TAG_CANONICAL: u8 = 1; +const HASH_TAG_EXACT_F64: u8 = 2; + +/// Hash a number whose exact value is the integer `value`. +/// +/// Every numeric representation routes integers here, so `5`, `5.0`, `5n` and +/// `10/2` produce one hash without allocating a [`BigInt`] to say so. +pub fn hash_exact_i64(value: i64, state: &mut H) { + HASH_TAG_EXACT_I64.hash(state); + value.hash(state); +} + +/// `5.0` answers `Some(5)`; `2.5`, `1e300`, `inf` and `NaN` answer `None`. +#[must_use] +pub fn exact_f64_to_i64(value: f64) -> Option { + // `i64::MIN` is exactly -2^63, so negating it gives the first f64 above + // `i64::MAX`. The bound is exclusive because `i64::MAX` itself rounds up + // to 2^63 when converted. + const LOWER: f64 = i64::MIN as f64; + const UPPER: f64 = -LOWER; + + // `fract` is NaN for infinities and NaN, so both fail this comparison. + if value.fract() == 0.0 && (LOWER..UPPER).contains(&value) { + Some(value as i64) + } else { + None + } +} + +impl Hash for AdvancedNumber { + fn hash(&self, state: &mut H) { + if let Some(value) = self.as_exact_i64() { + hash_exact_i64(value, state); + return; + } + + // A value an `f64` represents exactly hashes from its bits, which + // keeps float keys off the allocating path entirely. Only the values + // no float can express build the exact rational form. + if let Some(value) = self.as_exact_f64() { + HASH_TAG_EXACT_F64.hash(state); + // Every NaN compares equal here, so every NaN must hash alike + // regardless of the payload bits it carries. + let bits = if value.is_nan() { + f64::NAN.to_bits() + } else { + value.to_bits() + }; + bits.hash(state); + return; + } + + HASH_TAG_CANONICAL.hash(state); + self.canonical().hash(state); + } +} + +impl Neg for AdvancedNumber { + type Output = Self; + + fn neg(self) -> Self::Output { + match self { + Self::Int(i) => i.neg().into(), + Self::Float(f) => f.neg().into(), + Self::Rational(r) => r.into_inner().neg().into(), + Self::Complex(c) => c.neg().into(), + } + } +} + +trait Unbox { + type Output; + fn unbox(self) -> Self::Output; +} + +impl Unbox for ExactFraction { + type Output = BigRational; + fn unbox(self) -> Self::Output { + self.into_inner() + } +} + +impl<'a> Unbox for &'a ExactFraction { + type Output = &'a BigRational; + fn unbox(self) -> Self::Output { + self + } +} + +impl AsRef for ExactFraction { + fn as_ref(&self) -> &BigRational { + self + } +} + +#[derive(Debug)] +pub struct BinaryOperatorError(String); + +impl fmt::Display for BinaryOperatorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for BinaryOperatorError {} + +impl BinaryOperatorError { + pub fn new(message: String) -> Self { + Self(message) + } + + pub fn undefined_operation(operator: &str, left: &StaticType, right: &StaticType) -> Self { + Self(format!( + "operator {operator} is not defined for {left} and {right}" + )) + } +} + +macro_rules! impl_binary_operator { + ($self:ty, $other:ty, $trait:ident, $method:ident) => { + impl $trait<$other> for $self { + type Output = Result; + fn $method(self, other: $other) -> Self::Output { + Ok(match (self, other) { + // Integer + (AdvancedNumber::Int(left), AdvancedNumber::Int(right)) => { + AdvancedNumber::Int($trait::$method(left, right)) + } + // Complex + (AdvancedNumber::Complex(left), right) => { + AdvancedNumber::Complex($trait::$method(left, right.to_complex())) + } + (left, AdvancedNumber::Complex(right)) => { + AdvancedNumber::Complex($trait::$method(left.to_complex(), right)) + } + // Float + // NOTE: these `expect` calls are safe because complex has already been handled + (AdvancedNumber::Float(left), right) => { + AdvancedNumber::Float($trait::$method(left, right.expect_f64())) + } + (left, AdvancedNumber::Float(right)) => { + AdvancedNumber::Float($trait::$method(left.expect_f64(), right)) + } + // Rational + // NOTE: these `expect` calls are safe because complex and float are handled + (left, AdvancedNumber::Rational(right)) => AdvancedNumber::rational( + $trait::$method(left.expect_rational(), right.unbox()), + ), + (AdvancedNumber::Rational(left), right) => AdvancedNumber::rational( + $trait::$method(left.unbox(), right.expect_rational()), + ), + }) + } + } + }; +} + +impl_binary_operator!(AdvancedNumber, AdvancedNumber, Add, add); +impl_binary_operator!(AdvancedNumber, AdvancedNumber, Sub, sub); +impl_binary_operator!(AdvancedNumber, AdvancedNumber, Mul, mul); + +/// Returns `true` for the number kinds that use exact (integer/rational) +/// arithmetic. +fn is_exact(n: &AdvancedNumber) -> bool { + matches!(n, AdvancedNumber::Int(_) | AdvancedNumber::Rational(_)) +} + +impl Rem for AdvancedNumber { + type Output = Result; + + fn rem(self, rhs: Self) -> Self::Output { + // Exact arithmetic cannot represent a zero-divisor result. Number + // operations deliberately fall back to IEEE floating-point values in + // that case, just like exact division does. + if is_exact(&self) && is_exact(&rhs) && rhs.is_zero() { + return Ok(Self::Float( + self.to_f64().unwrap_or(f64::NAN) % rhs.to_f64().unwrap_or(f64::NAN), + )); + } + Ok(match (self, rhs) { + // Integer + (Self::Int(left), Self::Int(right)) => Self::Int(left % right), + // Complex + (Self::Complex(left), right) => Self::Complex(left % right.to_complex()), + (left, Self::Complex(right)) => Self::Complex(left.to_complex() % right), + // Float + (Self::Float(left), right) => Self::Float(left % right.expect_f64()), + (left, Self::Float(right)) => Self::Float(left.expect_f64() % right), + // Rational + (left, Self::Rational(right)) => Self::rational(left.expect_rational() % right.unbox()), + (Self::Rational(left), right) => Self::rational(left.unbox() % right.expect_rational()), + }) + } +} + +impl Rem<&Self> for AdvancedNumber { + type Output = Result; + + fn rem(self, rhs: &Self) -> Self::Output { + self % rhs.clone() + } +} + +impl Rem for &AdvancedNumber { + type Output = Result; + + fn rem(self, rhs: AdvancedNumber) -> Self::Output { + self.clone() % rhs + } +} + +impl Rem<&AdvancedNumber> for &AdvancedNumber { + type Output = Result; + + fn rem(self, rhs: &AdvancedNumber) -> Self::Output { + self.clone() % rhs.clone() + } +} + +impl Div<&AdvancedNumber> for &AdvancedNumber { + type Output = AdvancedNumber; + + fn div(self, rhs: &AdvancedNumber) -> Self::Output { + match (self.to_rational(), rhs.to_rational()) { + (Some(left), Some(right)) if !right.is_zero() => AdvancedNumber::rational(left / right), + _ => match (self.to_f64(), rhs.to_f64()) { + (Some(left), Some(right)) => AdvancedNumber::Float(left / right), + _ => AdvancedNumber::Complex(self.to_complex() / rhs.to_complex()), + }, + } + } +} + +impl Div for AdvancedNumber { + type Output = Result; + + fn div(self, rhs: Self) -> Self::Output { + Ok(&self / &rhs) + } +} +impl Div<&Self> for AdvancedNumber { + type Output = Result; + + fn div(self, rhs: &Self) -> Self::Output { + Ok(&self / rhs) + } +} +impl Div for &AdvancedNumber { + type Output = Result; + + fn div(self, rhs: AdvancedNumber) -> Self::Output { + Ok(self / &rhs) + } +} + +impl AdvancedNumber { + /// The exact value as an `i64`, when this number is a real integer that + /// fits one. `5n`, `5.0`, `10/2` and `5+0i` all answer `Some(5)`, while + /// `2.5`, `1e300`, `inf` and `5+1i` answer `None`. + /// + /// Every representation of one integer answers the same `Some`, which is + /// what lets [`Hash`] skip building a [`CanonicalNumber`] for it. + #[must_use] + pub fn as_exact_i64(&self) -> Option { + match self { + Self::Int(value) => value.to_i64(), + Self::Float(value) => exact_f64_to_i64(*value), + // A reduced rational is an integer exactly when its denominator is + // one, which `is_integer` checks without dividing. + Self::Rational(value) => value.is_integer().then(|| value.numer().to_i64())?, + Self::Complex(value) => (value.im == 0.0).then(|| exact_f64_to_i64(value.re))?, + } + } + + /// The value as the `f64` that represents it *exactly*, or `None` when + /// the conversion would lose anything. One step up from + /// [`Self::as_exact_i64`]: a float always qualifies, a rational only when + /// its denominator is a power of two that survives the mantissa. + /// + /// Equal values agree here, because exactness is a property of the number + /// and not of the variant holding it: `0.5` and `1n/2n` both answer + /// `Some(0.5)`, so both reach the same hash. + fn as_exact_f64(&self) -> Option { + match self { + Self::Int(value) => { + let candidate = value.to_f64()?; + (BigInt::from_f64(candidate).as_ref() == Some(value)).then_some(candidate) + } + Self::Float(value) => Some(*value), + Self::Rational(value) => { + let candidate = value.to_f64()?; + BigRational::from_float(candidate)? + .eq(value.as_ref()) + .then_some(candidate) + } + Self::Complex(value) => (value.im == 0.0).then_some(value.re), + } + } + + fn canonical(&self) -> CanonicalNumber { + match self { + Self::Int(value) => CanonicalNumber { + real: CanonicalScalar::Finite(BigRational::from_integer(value.clone())), + imaginary: CanonicalScalar::zero(), + }, + Self::Float(value) => CanonicalNumber { + real: CanonicalScalar::from_float(*value), + imaginary: CanonicalScalar::zero(), + }, + Self::Rational(value) => CanonicalNumber { + real: CanonicalScalar::Finite(value.as_ref().clone()), + imaginary: CanonicalScalar::zero(), + }, + Self::Complex(value) => CanonicalNumber { + real: CanonicalScalar::from_float(value.re), + imaginary: CanonicalScalar::from_float(value.im), + }, + } + } + + #[must_use] + pub fn complex(re: f64, im: f64) -> Self { + Self::Complex(Complex64 { re, im }) + } + + #[must_use] + /// Wrap an exact fraction, collapsing a denominator of 1 back to + /// [`Self::Int`]. Division and the remainders all land here, so without + /// the collapse `4n / 2n` stays a `Rational` that prints as `2` but no + /// longer matches the `Int` arm any consumer switches on. + pub fn rational(rat: BigRational) -> Self { + if rat.is_integer() { + return Self::Int(rat.to_integer()); + } + Self::Rational(ExactFraction(Box::new(rat))) + } + + pub fn static_type(&self) -> StaticType { + StaticType::Number + } + + #[must_use] + pub fn is_zero(&self) -> bool { + match self { + Self::Int(i) => i.is_zero(), + Self::Float(f) => *f == 0.0, + Self::Rational(r) => r.is_zero(), + Self::Complex(c) => c.is_zero(), + } + } + + pub fn checked_rem_euclid(self, rhs: &Self) -> Result { + if matches!(self, Self::Complex(_)) || matches!(rhs, Self::Complex(_)) { + return Err(BinaryOperatorError::undefined_operation( + "%%", + &self.static_type(), + &rhs.static_type(), + )); + } + if rhs.is_zero() { + return Ok(Self::Float( + self.to_f64() + .unwrap_or(f64::NAN) + .rem_euclid(rhs.to_f64().unwrap_or(f64::NAN)), + )); + } + if let (Some(left), Some(right)) = (self.to_rational(), rhs.to_rational()) { + let mut remainder = left % &right; + if remainder.is_negative() { + remainder += right.abs(); + } + return Ok(Self::rational(remainder)); + } + Ok(Self::Float( + self.to_f64() + .expect("complex operands were rejected") + .rem_euclid(rhs.to_f64().expect("complex operands were rejected")), + )) + } + + pub fn floor_div(self, rhs: Self) -> Result { + Ok(self.div(rhs)?.floor()) + } + + /// Raise an integer base to a (possibly negative) integer exponent. + /// A negative exponent yields the reciprocal as a rational; `pow` has + /// already rejected the `0 ^ negative` pair that would divide by zero. + fn int_pow(base: &BigInt, exponent: &BigInt) -> Result { + if exponent.is_negative() { + let denominator = num::pow::Pow::pow(base.clone(), exponent.magnitude()); + Ok(Self::rational(BigRational::new( + BigInt::from(1), + denominator, + ))) + } else { + Ok(Self::Int(num::pow::Pow::pow( + base.clone(), + exponent.magnitude(), + ))) + } + } + + pub fn pow(self, rhs: Self) -> Result { + // Reject astronomically large integer exponents up front: an exponent + // that doesn't fit in u32 would produce a result too large to compute + // in finite time. Without this guard, `2 ^ i64::MAX` hangs the VM. + const MAX_EXPONENT_BITS: u64 = 32; + let too_large = match &rhs { + Self::Int(b) => b.magnitude().bits() > MAX_EXPONENT_BITS, + Self::Rational(p) if p.is_integer() => p.numer().magnitude().bits() > MAX_EXPONENT_BITS, + _ => false, + }; + if too_large { + return Err(BinaryOperatorError::new( + "exponent too large to compute".to_string(), + )); + } + + // `0 ^ negative` is division by zero rather than a value. Both exact + // bases need the guard here: the rational arms below would otherwise + // panic inside num-rational with a zero denominator, taking the whole + // process down. Inexact zeroes keep their float infinity. + let exact_zero_base = match &self { + Self::Int(base) => base.is_zero(), + Self::Rational(base) => base.is_zero(), + Self::Float(_) | Self::Complex(_) => false, + }; + let exact_negative_exponent = match &rhs { + Self::Int(exponent) => exponent.is_negative(), + Self::Rational(exponent) => exponent.is_negative(), + Self::Float(_) | Self::Complex(_) => false, + }; + if exact_zero_base && exact_negative_exponent { + return Err(BinaryOperatorError::new("division by zero".to_string())); + } + + Ok(match (self, rhs) { + // Exact results first: an integer exponent keeps an exact base exact. + (Self::Int(base), Self::Int(exponent)) => return Self::int_pow(&base, &exponent), + (Self::Int(base), Self::Rational(exponent)) if exponent.is_integer() => { + return Self::int_pow(&base, &exponent.to_integer()); + } + (Self::Rational(base), Self::Int(exponent)) => { + Self::rational(num::pow::Pow::pow(&*base, exponent)) + } + (Self::Rational(base), Self::Rational(exponent)) + if exponent.is_integer() && exponent.to_i32().is_some() => + { + let exponent = exponent.to_i32().expect("checked by the match guard"); + Self::rational(base.pow(exponent)) + } + + // A complex operand on either side keeps the result complex. + (Self::Complex(base), exponent) => Self::Complex(base.powc(exponent.to_complex())), + (base, Self::Complex(exponent)) => Self::Complex(base.to_complex().powc(exponent)), + + // Everything else is real, so evaluate in floating point. + (base, exponent) => float_pow(base.expect_f64(), exponent.expect_f64()), + }) + } + + #[must_use] + pub fn to_complex(&self) -> Complex64 { + match self { + Self::Int(i) => Complex64::from(bigint_to_float(i)), + Self::Float(f) => Complex64::from(f), + Self::Rational(r) => rational_to_complex(r), + Self::Complex(c) => *c, + } + } + + #[must_use] + pub fn to_f64(&self) -> Option { + match self { + Self::Int(i) => Some(bigint_to_float(i)), + Self::Float(f) => Some(*f), + Self::Rational(r) => Some(rational_to_float(r)), + Self::Complex(_) => None, + } + } + + /// The `f64` value of a number that a caller has already established is not + /// complex. Panics otherwise. + fn expect_f64(&self) -> f64 { + self.to_f64().expect("cannot convert complex to float") + } + + /// The exact rational value of a number that a caller has already + /// established is an integer or a rational. Panics otherwise. + fn expect_rational(&self) -> BigRational { + self.to_rational().expect("cannot convert to rational") + } + + #[must_use] + pub fn to_rational(&self) -> Option { + match self { + Self::Int(i) => Some(BigRational::from_integer(i.clone())), + Self::Rational(r) => Some(BigRational::clone(&**r)), + Self::Float(_) | Self::Complex(_) => None, + } + } + + #[must_use] + pub fn abs(&self) -> Self { + match self { + Self::Int(i) => Self::Int(i.abs()), + Self::Float(f) => Self::Float(f.abs()), + Self::Rational(r) => Self::rational(r.abs()), + Self::Complex(c) => Self::Float(c.abs()), + } + } + + #[must_use] + pub fn signum(&self) -> Self { + match self { + Self::Int(i) => i.signum().into(), + Self::Float(f) => Self::Float(f.signum()), + Self::Rational(ratio) => Self::from(ratio.signum()), + Self::Complex(complex) => { + // I trust you Brian :crycat: + if complex.re.is_zero() && complex.im.is_zero() { + self.clone() + } else { + Self::Complex(complex / complex.norm()) + } + } + } + } +} + +macro_rules! implement_rounding { + ($method:ident) => { + impl AdvancedNumber { + #[must_use] + pub fn $method(&self) -> AdvancedNumber { + match self { + AdvancedNumber::Int(i) => AdvancedNumber::Int(i.clone()), + AdvancedNumber::Float(f) => { + let f = f.$method(); + if let Some(i) = BigInt::from_f64(f) { + AdvancedNumber::Int(i) + } else { + AdvancedNumber::Float(f) + } + } + AdvancedNumber::Rational(r) => AdvancedNumber::Int(r.$method().to_integer()), + AdvancedNumber::Complex(c) => { + Complex::new(c.re.$method(), c.im.$method()).into() + } + } + } + } + }; +} + +implement_rounding!(ceil); +implement_rounding!(floor); +implement_rounding!(round); + +#[derive(thiserror::Error, Debug)] +pub enum NumberToFloatError { + #[error("cannot convert {0} to float")] + UnsupportedType(StaticType), + #[error("cannot convert {0} to float")] + UnsupportedValue(AdvancedNumber), +} + +impl TryFrom<&AdvancedNumber> for f64 { + type Error = NumberToFloatError; + + fn try_from(value: &AdvancedNumber) -> Result { + match value { + AdvancedNumber::Int(value) => value.to_f64(), + AdvancedNumber::Float(f) => Some(*f), + AdvancedNumber::Rational(r) => r.to_f64(), + AdvancedNumber::Complex(_) => { + return Err(Self::Error::UnsupportedType(value.static_type())); + } + } + .ok_or_else(|| Self::Error::UnsupportedValue(value.clone())) + } +} + +impl fmt::Display for AdvancedNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Int(i) => write!(f, "{i}"), + Self::Float(ff) => { + let mut buffer = ryu::Buffer::new(); + f.write_str(buffer.format(*ff)) + } + Self::Rational(r) => write!(f, "{r}"), + Self::Complex(r) => write!(f, "{r}"), + } + } +} + +/// `base ^ exponent` in floating point, escaping to the complex plane when a +/// negative base is raised to a fractional power: `(-8.0) ^ 2.0` is `64.0`, +/// but `(-8.0) ^ 0.5` has no real value. +fn float_pow(base: f64, exponent: f64) -> AdvancedNumber { + // `fract()` is NaN for an infinite or NaN exponent, which compares + // unequal to zero and would send a finite negative base into the complex + // plane. `powf` already answers those correctly. + if base < 0.0 && exponent.is_finite() && exponent.fract() != 0.0 { + AdvancedNumber::Complex(Complex64::from(base).powf(exponent)) + } else { + AdvancedNumber::Float(base.powf(exponent)) + } +} + +fn rational_to_float(r: &BigRational) -> f64 { + r.to_f64().unwrap_or(f64::NAN) +} + +fn bigint_to_float(value: &BigInt) -> f64 { + value.to_f64().unwrap_or_else(|| { + if value.is_negative() { + f64::NEG_INFINITY + } else { + f64::INFINITY + } + }) +} + +fn rational_to_complex(r: &BigRational) -> Complex { + Complex::from(r.to_f64().unwrap_or(f64::NAN)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + fn hash(value: &AdvancedNumber) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() + } + + #[test] + fn equality_and_hash_use_exact_numeric_values() { + let integer = AdvancedNumber::Int(BigInt::from(1)); + let float = AdvancedNumber::Float(1.0); + let complex = AdvancedNumber::complex(1.0, -0.0); + + assert_eq!(integer, float); + assert_eq!(float, complex); + assert_eq!(hash(&integer), hash(&float)); + assert_eq!(hash(&float), hash(&complex)); + + let tenth = AdvancedNumber::rational(BigRational::new(1.into(), 10.into())); + assert_ne!(tenth, AdvancedNumber::Float(0.1)); + } + + #[test] + fn every_representation_of_one_integer_hashes_alike() { + let five = [ + AdvancedNumber::Int(BigInt::from(5)), + AdvancedNumber::Float(5.0), + AdvancedNumber::complex(5.0, -0.0), + ]; + + for value in &five { + assert_eq!(value.as_exact_i64(), Some(5)); + assert_eq!(*value, five[0]); + assert_eq!(hash(value), hash(&five[0])); + } + } + + /// A whole fraction has no rational representation to hash: the + /// constructor is the only way to build the variant and it hands back an + /// integer instead, which is what keeps every consumer that switches on + /// the variant honest. + #[test] + fn a_whole_fraction_becomes_an_integer() { + let five = AdvancedNumber::rational(BigRational::new(10.into(), 2.into())); + assert!(matches!(five, AdvancedNumber::Int(_))); + assert_eq!(five, AdvancedNumber::Int(BigInt::from(5))); + + let whole = AdvancedNumber::from(BigRational::from_integer(7.into())); + assert!(matches!(whole, AdvancedNumber::Int(_))); + + // a genuine fraction is left alone + let half = AdvancedNumber::rational(BigRational::new(1.into(), 2.into())); + assert!(matches!(half, AdvancedNumber::Rational(_))); + } + + #[test] + fn values_off_the_integer_fast_path_still_agree() { + let huge = AdvancedNumber::Int(BigInt::from(u64::MAX) * BigInt::from(u64::MAX)); + let half = AdvancedNumber::rational(BigRational::new(1.into(), 2.into())); + + assert_eq!(huge.as_exact_i64(), None); + assert_eq!(half.as_exact_i64(), None); + + // 0.5 is exactly 1/2, so the two representations stay interchangeable + // on the canonical path. + assert_eq!(half, AdvancedNumber::Float(0.5)); + assert_eq!(hash(&half), hash(&AdvancedNumber::Float(0.5))); + assert_ne!(hash(&huge), hash(&half)); + } + + #[test] + fn integer_fast_path_stops_at_the_i64_boundaries() { + assert_eq!(exact_f64_to_i64(i64::MIN as f64), Some(i64::MIN)); + // 2^63 is one past i64::MAX, so it falls back instead of wrapping. + assert_eq!(exact_f64_to_i64(-(i64::MIN as f64)), None); + assert_eq!(exact_f64_to_i64(f64::INFINITY), None); + assert_eq!(exact_f64_to_i64(f64::NAN), None); + assert_eq!(exact_f64_to_i64(2.5), None); + + // Negative zero is zero, so it must not take a bucket of its own. + assert_eq!(exact_f64_to_i64(-0.0), Some(0)); + assert_eq!( + hash(&AdvancedNumber::Float(-0.0)), + hash(&AdvancedNumber::Int(BigInt::from(0))) + ); + } + + #[test] + fn nan_is_equal_and_sorts_after_infinity() { + let nan = AdvancedNumber::Float(f64::NAN); + let complex_nan = AdvancedNumber::complex(f64::NAN, 0.0); + let infinity = AdvancedNumber::Float(f64::INFINITY); + + assert_eq!(nan, nan.clone()); + assert_eq!(nan, complex_nan); + assert!(nan > infinity); + } + + #[test] + fn complex_order_is_lexicographic() { + assert!(AdvancedNumber::complex(2.0, 0.0) > AdvancedNumber::complex(1.0, 100.0)); + assert!(AdvancedNumber::complex(1.0, 2.0) < AdvancedNumber::complex(1.0, 3.0)); + } +} diff --git a/ndc_vm/src/value/numeric.rs b/ndc_vm/src/value/numeric.rs new file mode 100644 index 00000000..c2e4f259 --- /dev/null +++ b/ndc_vm/src/value/numeric.rs @@ -0,0 +1,250 @@ +use ndc_core::StaticType; +use num::ToPrimitive; +use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; + +use super::number::{AdvancedNumber, hash_exact_i64}; + +/// The runtime representation mode of a numeric [`super::Value`]. +/// +/// This is an operational distinction used for overload selection and result +/// promotion. It does not define a subtype relationship between Andy types. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NumericMode { + Int, + Float, + Number, +} + +impl NumericMode { + pub const ALL: [Self; 3] = [Self::Int, Self::Float, Self::Number]; + + pub fn static_type(self) -> StaticType { + match self { + Self::Int => StaticType::Int, + Self::Float => StaticType::Float, + Self::Number => StaticType::Number, + } + } + + /// Return the numeric mode used by an operation combining both modes. + pub fn promote(self, other: Self) -> Self { + if self == Self::Number || other == Self::Number { + Self::Number + } else if self == Self::Float || other == Self::Float { + Self::Float + } else { + Self::Int + } + } +} + +/// A borrowed, non-subtyping view over the three numeric [`super::Value`] variants. +#[derive(Clone, Copy, Debug)] +pub enum NumericRef<'a> { + Int(i64), + Float(f64), + Number(&'a AdvancedNumber), +} + +impl<'a> NumericRef<'a> { + pub fn mode(self) -> NumericMode { + match self { + Self::Int(_) => NumericMode::Int, + Self::Float(_) => NumericMode::Float, + Self::Number(_) => NumericMode::Number, + } + } + + pub fn as_number(self) -> Option<&'a AdvancedNumber> { + match self { + Self::Number(value) => Some(value), + Self::Int(_) | Self::Float(_) => None, + } + } + + /// Promote a primitive numeric value to `f64` without accepting `Number`. + pub fn to_primitive_float(self) -> Option { + match self { + Self::Int(value) => Some(value as f64), + Self::Float(value) => Some(value), + Self::Number(_) => None, + } + } + + /// Convert a real numeric value to `f64`. + /// + /// Complex `Number` values cannot be represented and return `None`. + pub fn to_f64(self) -> Option { + match self { + Self::Int(value) => Some(value as f64), + Self::Float(value) => Some(value), + Self::Number(value) => value.to_f64(), + } + } + + /// Convert to `i64` using the language's checked, truncating `int()` policy. + pub fn to_i64_truncating(self) -> Option { + match self { + Self::Int(value) => Some(value), + Self::Float(value) => float_to_i64(value), + Self::Number(value) => match value { + AdvancedNumber::Int(value) => value.to_i64(), + AdvancedNumber::Float(value) => float_to_i64(*value), + AdvancedNumber::Rational(value) => value.to_integer().to_i64(), + AdvancedNumber::Complex(_) => None, + }, + } + } + + /// Promote this value to the owned representation used by `Number`. + pub fn to_advanced_number(self) -> AdvancedNumber { + match self { + Self::Int(value) => AdvancedNumber::Int(num::BigInt::from(value)), + Self::Float(value) => AdvancedNumber::Float(value), + Self::Number(value) => value.clone(), + } + } + + pub fn compare(self, other: Self) -> Ordering { + match (self, other) { + (Self::Int(left), Self::Int(right)) => left.cmp(&right), + (Self::Float(left), Self::Float(right)) => compare_floats(left, right), + (Self::Int(left), Self::Float(right)) => compare_int_float(left, right), + (Self::Float(left), Self::Int(right)) => compare_int_float(right, left).reverse(), + // A `Number` on either side falls back to its exact ordering. The + // primitive side is promoted, but a `Number` is never cloned. + (Self::Number(left), Self::Number(right)) => exact_cmp(left, right), + (Self::Number(left), right) => exact_cmp(left, &right.to_advanced_number()), + (left, Self::Number(right)) => exact_cmp(&left.to_advanced_number(), right), + } + } +} + +impl PartialEq for NumericRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.compare(*other) == Ordering::Equal + } +} + +impl Eq for NumericRef<'_> {} + +impl PartialOrd for NumericRef<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for NumericRef<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.compare(*other) + } +} + +impl Hash for NumericRef<'_> { + fn hash(&self, state: &mut H) { + match self { + // Both arms agree with `AdvancedNumber`'s hash, which routes + // integers through `hash_exact_i64` too, so `5`, `5.0` and `5n` + // land in the same bucket. + Self::Int(value) => hash_exact_i64(*value, state), + Self::Float(value) => AdvancedNumber::Float(*value).hash(state), + Self::Number(value) => value.hash(state), + } + } +} + +fn exact_cmp(left: &AdvancedNumber, right: &AdvancedNumber) -> Ordering { + left.partial_cmp(right) + .expect("AdvancedNumber values are totally ordered") +} + +/// Truncate toward zero, answering `None` when the result would not fit. +/// Truncating first makes this the exact conversion of a whole number, so it +/// reuses that bounds check rather than allocating a `BigInt` to range-test. +fn float_to_i64(value: f64) -> Option { + super::number::exact_f64_to_i64(value.trunc()) +} + +/// Match `AdvancedNumber`'s total ordering without constructing exact rational +/// representations for two values that are already stored as floats. +fn compare_floats(left: f64, right: f64) -> Ordering { + match (left.is_nan(), right.is_nan()) { + (true, true) => Ordering::Equal, + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (false, false) => left + .partial_cmp(&right) + .expect("non-NaN floats are totally ordered"), + } +} + +/// Compare an `i64` to an `f64` exactly, without first allocating a `BigInt` +/// and exact `BigRational` for their `AdvancedNumber` representations. +fn compare_int_float(integer: i64, float: f64) -> Ordering { + const I64_MIN_AS_F64: f64 = i64::MIN as f64; + const I64_UPPER_BOUND_AS_F64: f64 = i64::MAX as f64; + + if float.is_nan() || float >= I64_UPPER_BOUND_AS_F64 { + return Ordering::Less; + } + if float < I64_MIN_AS_F64 { + return Ordering::Greater; + } + + let truncated = float.trunc() as i64; + match integer.cmp(&truncated) { + Ordering::Equal if float.fract() == 0.0 => Ordering::Equal, + Ordering::Equal if float.is_sign_positive() => Ordering::Less, + Ordering::Equal => Ordering::Greater, + ordering => ordering, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::hash_map::DefaultHasher; + + fn hash(value: NumericRef<'_>) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() + } + + #[test] + fn equal_numeric_refs_hash_alike_across_modes() { + let five = AdvancedNumber::Int(5.into()); + let five_halves = AdvancedNumber::rational(num::BigRational::new(5.into(), 2.into())); + + for reference in [NumericRef::Float(5.0), NumericRef::Number(&five)] { + assert_eq!(reference, NumericRef::Int(5)); + assert_eq!(hash(reference), hash(NumericRef::Int(5))); + } + + // Values that miss the integer fast path still have to agree. + assert_eq!(NumericRef::Float(2.5), NumericRef::Number(&five_halves)); + assert_eq!( + hash(NumericRef::Float(2.5)), + hash(NumericRef::Number(&five_halves)) + ); + assert_ne!(hash(NumericRef::Int(5)), hash(NumericRef::Float(2.5))); + } + + #[test] + fn numeric_modes_promote_by_runtime_representation() { + assert_eq!(NumericMode::Int.promote(NumericMode::Int), NumericMode::Int); + assert_eq!( + NumericMode::Int.promote(NumericMode::Float), + NumericMode::Float + ); + assert_eq!( + NumericMode::Float.promote(NumericMode::Int), + NumericMode::Float + ); + for mode in NumericMode::ALL { + assert_eq!(mode.promote(NumericMode::Number), NumericMode::Number); + assert_eq!(NumericMode::Number.promote(mode), NumericMode::Number); + } + } +} diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index a994a334..7baf824c 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -493,7 +493,7 @@ impl Vm { let end = if bounded { let v = self.stack.pop().expect("stack underflow"); let Value::Int(n) = v else { - return Err(VmError::new("Integer too large for range bounds", span)); + return Err(VmError::new(bad_range_bound(&v), span)); }; Some(n) } else { @@ -501,7 +501,7 @@ impl Vm { }; let start = self.stack.pop().expect("stack underflow"); let Value::Int(start) = start else { - return Err(VmError::new("Integer too large for range bounds", span)); + return Err(VmError::new(bad_range_bound(&start), span)); }; let iter: Rc> = match (inclusive, end) { (_, None) => Rc::new(RefCell::new(UnboundedRangeIter::new(start))), @@ -1247,3 +1247,17 @@ impl VmCallable<'_> { self.vm.call_callback(self.function.clone(), args) } } + +/// Explain why a range bound was rejected. Only an integer too big for an +/// `Int` is a size problem; every other non-`Int` bound is a type problem, and +/// since `n` literals are the only way to write a big integer, a small +/// `Number` bound is now the common case. +fn bad_range_bound(value: &Value) -> String { + if let Some(number) = value.as_number() + && matches!(number, crate::value::AdvancedNumber::Int(_)) + && number.as_exact_i64().is_none() + { + return "Integer too large for range bounds".to_string(); + } + format!("range bounds must be Int, got {}", value.diagnostic_type()) +} diff --git a/tests/functional/programs/001_math/004_integer_division.ndc b/tests/functional/programs/001_math/004_integer_division.ndc index ff45227b..84c0c1c4 100644 --- a/tests/functional/programs/001_math/004_integer_division.ndc +++ b/tests/functional/programs/001_math/004_integer_division.ndc @@ -1,6 +1,6 @@ assert_eq(10 / 2, 5); assert_eq(10 \ 2, 5); -assert_eq(10 / 3, 10 / 3); +assert_eq(10 / 3, 3); -assert_eq((10 / 3).denominator, 3); -assert_eq((10 / 3).numerator, 10); +assert_eq((10n / 3n).denominator, 3n); +assert_eq((10n / 3n).numerator, 10n); diff --git a/tests/functional/programs/001_math/010_rational_numbers.ndc b/tests/functional/programs/001_math/010_rational_numbers.ndc index ed9f6e72..328469bc 100644 --- a/tests/functional/programs/001_math/010_rational_numbers.ndc +++ b/tests/functional/programs/001_math/010_rational_numbers.ndc @@ -1 +1,2 @@ -assert_eq(123 ^ 1234, 87681408214164476970365205228763109219898888878881068771208558661744652944973794097995863371113304803648974825541467427939839526377710059863431476309143764813924927562640894119948501580418029986500720998388787813370525431166848874516198653313025239228397155448068660083441510113309297213377130812540882569036330888360587252320502710668238917924819256177085547151628452956575620209396410425802528274173423237198788850317119387183597639340271827009446479814212636428433508905393502570143050733004341365267464673083511407846749856768559082044756465557382458441298173247590732260532362837781261273044790732964572472906028786735769068002908334950629529238494229388773631509587346276421451934859511090185201687935820179348745078114760498326353080922545842429714078866368666174811436292368322013344004655977058760977542054124836643126063114763695548545776130407210820780894324241690914009874920841120259000224443381313232432773323198180041283907691642196083913253441176164973189724729319313191910270523504758501858336459076757618992839333332511497597480067748947646390153654975397979573562246663761509470719229703024274190910777440709851016954053496016760952179721806135963774666054115617271706189246258265115943067237016490399563635443442846739014850282195618521120535565188025906868874107944865094967772873343090181992187951403546897725021536947680972091031688467020738605076457274796500971999661728798303030972853189743552799579684844828491444308484526518267218257921241625499052436609254699010727463618576326919716488239497546381103193111733548334284388280545773071212732103370578217302060632234196305992004053401681566112227114812691817085587946583104659271921923562874223100900959335802212944702005164243039866416649440564428814269313203471855669910522511335546803534892285510883626302419238150232750213370699115557243071043712094583781097657284873633992611116149199287419640896041431121935521412322305706094971610821567279856521112109486451031119237844839149647788096234240268663046528172307744046671563379062495488515946010782381404515931631412299705016057392361243778777836765937047145439248470525821254187111275601658651848526621397680714909511070312403055710824426638837020346610435667546953236125866025757795263762436864502672294404692055020881153207578385898388026761498803446534437001436910282915105450240960086694231786588797080733683534366526961049764901226371507061043411117058668037169788357610458683641714305519588901316259335868882461093614111174112266777064725000599519534220824847284140314691213633043592112726822825400146132529759565779050658199333210486870286809); +assert_eq(2n ^ 100n, 1267650600228229401496703205376n); +assert_eq(10n / 4n, 5n / 2n); diff --git a/tests/functional/programs/001_math/013_negative_integer_exponents.ndc b/tests/functional/programs/001_math/013_negative_integer_exponents.ndc index 0ff1e885..73ef034e 100644 --- a/tests/functional/programs/001_math/013_negative_integer_exponents.ndc +++ b/tests/functional/programs/001_math/013_negative_integer_exponents.ndc @@ -1 +1 @@ -assert_eq(5 ^ -1, 1/5); +assert_eq(5n ^ -1n, 1n/5n); diff --git a/tests/functional/programs/001_math/014_exponentiation_right_associative.ndc b/tests/functional/programs/001_math/014_exponentiation_right_associative.ndc index d33bd3ef..4e379971 100644 --- a/tests/functional/programs/001_math/014_exponentiation_right_associative.ndc +++ b/tests/functional/programs/001_math/014_exponentiation_right_associative.ndc @@ -1,2 +1,2 @@ // Exponentiation is defined as being right associative -assert_eq(2 ^ 3 ^ 4, 2417851639229258349412352); +assert_eq(2n ^ 3n ^ 4n, 2417851639229258349412352n); diff --git a/tests/functional/programs/001_math/019_bit_operations.ndc b/tests/functional/programs/001_math/019_bit_operations.ndc index 1b29031c..1c6dabf0 100644 --- a/tests/functional/programs/001_math/019_bit_operations.ndc +++ b/tests/functional/programs/001_math/019_bit_operations.ndc @@ -26,6 +26,5 @@ assert(~0 == -1); assert(~1 == -2); assert(~1234 == ~~-1235); // LOL -// bigintegers -assert((2^65) | (2^65) == (2^65)); - +// Int bit operations stay within the primitive mode. +assert((2^62) | (2^62) == (2^62)); diff --git a/tests/functional/programs/001_math/022_spaceship.ndc b/tests/functional/programs/001_math/022_spaceship.ndc index 3ae5ae8c..3051e476 100644 --- a/tests/functional/programs/001_math/022_spaceship.ndc +++ b/tests/functional/programs/001_math/022_spaceship.ndc @@ -5,9 +5,9 @@ assert_eq(5 <=> 6, -1); assert_eq(6 <=> 5, 1); assert_eq(5 <=> 5, 0); assert_eq(9223372036854775807 <=> 9223372036854775806, 1); // i64 max -assert_eq(-9223372036854775808 <=> -9223372036854775807, -1); // i64 min -assert_eq(9223372036854775808 <=> -9223372036854775808, 1); // Outside i64 -assert_eq(-9223372036854775809 <=> 9223372036854775807, -1); +assert_eq((-9223372036854775807 - 1) <=> -9223372036854775807, -1); // i64 min +assert_eq(9223372036854775808n <=> -9223372036854775808n, 1); // Outside i64 +assert_eq(-9223372036854775809n <=> 9223372036854775807, -1); // Strings (single and multiple characters) assert_eq("a" <=> "b", -1); @@ -35,9 +35,9 @@ assert_eq(0 <=> 0.1, -1); // zero < small positive float assert_eq(0 <=> -0.1, 1); // zero > small negative float // Rational Numbers -assert_eq((5 / 3) <=> (7 / 4), -1); // 5/3 < 7/4 -assert_eq((7 / 4) <=> (5 / 3), 1); // 7/4 > 5/3 -assert_eq((5 / 3) <=> (5 / 3), 0); // Equal rational numbers +assert_eq((5n / 3n) <=> (7n / 4n), -1); // 5/3 < 7/4 +assert_eq((7n / 4n) <=> (5n / 3n), 1); // 7/4 > 5/3 +assert_eq((5n / 3n) <=> (5n / 3n), 0); // Equal rational numbers // Complex Numbers assert_eq((1 + 2i) <=> (2 + 3i), -1); // Compare based on magnitude, or however complex comparison is defined @@ -79,7 +79,7 @@ assert_eq("b" >=< "a", -1); assert_eq("a" >=< "a", 0); assert_eq([1, 2, 3] >=< [1, 2, 4], 1); assert_eq((1, 2, 3) >=< (1, 2, 4), 1); -assert_eq((5 / 3) >=< (7 / 4), 1); +assert_eq((5n / 3n) >=< (7n / 4n), 1); assert_eq((1 + 2i) >=< (2 + 3i), 1); assert_eq(true >=< false, -1); assert_eq(false >=< true, 1); diff --git a/tests/functional/programs/001_math/025_modulo_by_zero_bigint.ndc b/tests/functional/programs/001_math/025_modulo_by_zero_bigint.ndc index 2c81be23..d20cd953 100644 --- a/tests/functional/programs/001_math/025_modulo_by_zero_bigint.ndc +++ b/tests/functional/programs/001_math/025_modulo_by_zero_bigint.ndc @@ -1,2 +1,2 @@ -// expect-error: division by zero -print(170141183460469231731687303715884105727 % 0); +print(170141183460469231731687303715884105727n % 0n); +// expect-output: NaN diff --git a/tests/functional/programs/001_math/026_floor_div_by_zero.ndc b/tests/functional/programs/001_math/026_floor_div_by_zero.ndc index 0c926052..7dd1b72e 100644 --- a/tests/functional/programs/001_math/026_floor_div_by_zero.ndc +++ b/tests/functional/programs/001_math/026_floor_div_by_zero.ndc @@ -1,4 +1,4 @@ // Floor division by zero promotes to a float and yields infinity, the same // as `/` (it must not panic). -print(5 \ 0); +print(5n \ 0n); // expect-output: inf diff --git a/tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc b/tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc index 7365e79c..e6577e4d 100644 --- a/tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc +++ b/tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc @@ -1,3 +1,3 @@ -// expect-error: division by zero -let a = 1 / 3; -print(a % 0); +let a = 1n / 3n; +print(a % 0n); +// expect-output: NaN diff --git a/tests/functional/programs/001_math/029_floor_div_by_zero_rational.ndc b/tests/functional/programs/001_math/029_floor_div_by_zero_rational.ndc index 74797972..9dfcd817 100644 --- a/tests/functional/programs/001_math/029_floor_div_by_zero_rational.ndc +++ b/tests/functional/programs/001_math/029_floor_div_by_zero_rational.ndc @@ -1,6 +1,6 @@ // Regression: rational and BigInt floor division by zero yields infinity, // not an error (matches `/`). -print((1 / 3) \ 0); -print(170141183460469231731687303715884105727 \ 0); +print((1n / 3n) \ 0n); +print(170141183460469231731687303715884105727n \ 0n); // expect-output: inf // expect-output: inf diff --git a/tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc b/tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc index 5bd2d126..2220682b 100644 --- a/tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc +++ b/tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc @@ -2,4 +2,4 @@ // used to build a rational with a zero denominator, which panicked in // num-rational ("denominator == 0"). It now reports a recoverable error. // expect-error: division by zero -print(0 ^ -1); +print(0n ^ -1n); diff --git a/tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc b/tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc index ff912d6f..8210d029 100644 --- a/tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc +++ b/tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc @@ -2,4 +2,4 @@ // exponent path (`2/-1` is an integer-valued rational). Used to panic // with "right hand side must not be negative"; now a recoverable error. // expect-error: division by zero -print(0 ^ (2/-1)); +print(0n ^ (2n/-1n)); diff --git a/tests/functional/programs/001_math/032_neg_abs_i64_min.ndc b/tests/functional/programs/001_math/032_neg_abs_i64_min.ndc index b24e23a9..0f2074ac 100644 --- a/tests/functional/programs/001_math/032_neg_abs_i64_min.ndc +++ b/tests/functional/programs/001_math/032_neg_abs_i64_min.ndc @@ -2,7 +2,8 @@ // range (`-i64::MIN` does not fit in an i64). `Int::neg` and `Int::abs` used // to call `i.neg()` / `i.abs()` directly, which panics in debug builds and // silently wraps in release. They now promote to BigInt, yielding the correct -// positive magnitude. -// expect-output: 9223372036854775808 9223372036854775808 9223372036854775808 +// positive magnitude. Int arithmetic now reports the overflow instead of +// promoting implicitly; Number remains arbitrary precision. +// expect-error: integer negation overflowed let min = -9223372036854775807 - 1; -print(-min, abs(min), abs(-min)); +print(-min); diff --git a/tests/functional/programs/001_math/033_numeric_modes.ndc b/tests/functional/programs/001_math/033_numeric_modes.ndc new file mode 100644 index 00000000..6da25220 --- /dev/null +++ b/tests/functional/programs/001_math/033_numeric_modes.ndc @@ -0,0 +1,40 @@ +// The three public numeric types are siblings. Every mixed arithmetic pair +// has an explicit overload and the result mode follows Int < Float < Number. +let int_int: Int = 1 + 2; +let int_float: Float = 1 + 2.5; +let float_int: Float = 2.5 + 1; +let float_float: Float = 1.5 + 2.5; +let int_number: Number = 1 + 2n; +let number_int: Number = 2n + 1; +let float_number: Number = 1.5 + 2n; +let number_float: Number = 2n + 1.5; +let number_number: Number = 1n + 2n; + +assert_eq(int_int, 3); +assert_eq(int_float, 3.5); +assert_eq(float_int, 3.5); +assert_eq(float_float, 4.0); +assert_eq(int_number, 3n); +assert_eq(number_int, 3n); +assert_eq(float_number, 3.5n); +assert_eq(number_float, 3.5n); +assert_eq(number_number, 3n); + +// Int division truncates, floor division rounds toward negative infinity, +// and the two remainder operators pair with those choices. +assert_eq(-7 / 2, -3); +assert_eq(-7 \ 2, -4); +assert_eq(-7 % 2, -1); +assert_eq(-7 %% 2, 1); +assert_eq(2 ^ 10, 1024); + +// Number division remains exact and all Number results remain wrapped. +let ratio: Number = 7n / 2n; +let floored: Number = -7n \ 2n; +assert_eq(ratio, 7n / 2n); +assert_eq(floored, -4n); +assert_eq(7n \ -2n, -4n); + +assert_eq(Number(3), 3n); +assert_eq(Number(3.5), 3.5n); +assert_eq(Number(3n), 3n); diff --git a/tests/functional/programs/001_math/034_exact_numeric_equality.ndc b/tests/functional/programs/001_math/034_exact_numeric_equality.ndc new file mode 100644 index 00000000..d5965eba --- /dev/null +++ b/tests/functional/programs/001_math/034_exact_numeric_equality.ndc @@ -0,0 +1,24 @@ +// Equal values compare and hash alike across all three public numeric modes. +assert(1 == 1.0); +assert(1.0 == 1n); +assert(1n == 1 + 0i); +assert_eq(%{1, 1.0, 1n, 1 + 0i}.len(), 1); + +// A finite Float is compared as its exact binary value, not approximately. +assert(0.1 != 1n / 10n); +assert(-0.0 == 0.0); + +// NaNs form one equal, hash-compatible value and sort after positive infinity. +let nan = 0.0 / 0.0; +let number_nan = 0n / 0n; +assert(nan == number_nan); +assert_eq(%{nan, number_nan}.len(), 1); +let negative_infinity = -1n / 0n; +let positive_infinity = 1n / 0n; +assert(negative_infinity < 0n); +assert(0n < positive_infinity); +assert(positive_infinity < number_nan); + +// Complex ordering is lexicographic: real part first, imaginary part second. +assert((2 + 0i) > (1 + 100i)); +assert((1 + 2i) < (1 + 3i)); diff --git a/tests/functional/programs/001_math/035_number_literals.ndc b/tests/functional/programs/001_math/035_number_literals.ndc new file mode 100644 index 00000000..ff110732 --- /dev/null +++ b/tests/functional/programs/001_math/035_number_literals.ndc @@ -0,0 +1,18 @@ +let decimal_integer: Number = 123456789123456789123456789n; +let decimal_float: Number = 1.25n; +let binary: Number = 0b101010n; +let octal: Number = 0o52n; +let hexadecimal: Number = 0x2an; +let imaginary_i: Number = 2i; +let imaginary_j: Number = 2j; + +assert_eq(decimal_integer, 123456789123456789123456789n); +assert_eq(decimal_float, Number(1.25)); +assert_eq(binary, 42n); +assert_eq(octal, 42n); +assert_eq(hexadecimal, 42n); +assert_eq(imaginary_i, imaginary_j); + +// Arbitrary-radix literals intentionally remain Int literals without a suffix. +let arbitrary_radix: Int = 16r2a; +assert_eq(arbitrary_radix, 42); diff --git a/tests/functional/programs/001_math/036_number_stdlib.ndc b/tests/functional/programs/001_math/036_number_stdlib.ndc new file mode 100644 index 00000000..25014ca4 --- /dev/null +++ b/tests/functional/programs/001_math/036_number_stdlib.ndc @@ -0,0 +1,25 @@ +let primitive_factorial: Int = factorial(20); +let advanced_factorial: Number = factorial(25n); +assert_eq(primitive_factorial, 2432902008176640000); +assert_eq(advanced_factorial, 15511210043330985984000000n); + +assert_eq(gcd(54, 24), 6); +assert_eq(lcm(21, 6), 42); +assert_eq(gcd(540000000000000000000n, 240000000000000000000n), 60000000000000000000n); +assert_eq(lcm(21n, 6n), 42n); + +assert_eq(ceil(7n / 2n), 4n); +assert_eq(floor(-7n / 2n), -4n); +assert_eq(round(7n / 2n), 4n); +assert_eq(abs(-9223372036854775808n), 9223372036854775808n); +assert_eq(abs_diff(1n, 3.5), 2.5n); + +assert_eq(real(2 + 3i), 2n); +assert_eq(imag(2 + 3i), 3n); +assert_eq(numerator(7n / 3n), 7n); +assert_eq(denominator(7n / 3n), 3n); + +assert_eq(sqrt(-1n), 1i); +let negative_log: Number = ln(-1n); +assert_eq(real(negative_log), 0n); +assert(imag(negative_log) > 3.14n); diff --git a/tests/functional/programs/001_math/037_oversized_int_literal.ndc b/tests/functional/programs/001_math/037_oversized_int_literal.ndc new file mode 100644 index 00000000..5c1c2bf1 --- /dev/null +++ b/tests/functional/programs/001_math/037_oversized_int_literal.ndc @@ -0,0 +1,2 @@ +// expect-error: use the advanced literal `9223372036854775808n` +print(9223372036854775808); diff --git a/tests/functional/programs/001_math/038_arbitrary_radix_number_suffix.ndc b/tests/functional/programs/001_math/038_arbitrary_radix_number_suffix.ndc new file mode 100644 index 00000000..dbeb3152 --- /dev/null +++ b/tests/functional/programs/001_math/038_arbitrary_radix_number_suffix.ndc @@ -0,0 +1,2 @@ +// expect-error: suffix is not supported on arbitrary-radix literals +print(16rffn); diff --git a/tests/functional/programs/001_math/039_removed_rational_type.ndc b/tests/functional/programs/001_math/039_removed_rational_type.ndc new file mode 100644 index 00000000..75c1747e --- /dev/null +++ b/tests/functional/programs/001_math/039_removed_rational_type.ndc @@ -0,0 +1,2 @@ +// expect-error: unknown type `Rational` +let value: Rational = 1n / 2n; diff --git a/tests/functional/programs/001_math/040_removed_complex_type.ndc b/tests/functional/programs/001_math/040_removed_complex_type.ndc new file mode 100644 index 00000000..97633b7f --- /dev/null +++ b/tests/functional/programs/001_math/040_removed_complex_type.ndc @@ -0,0 +1,2 @@ +// expect-error: unknown type `Complex` +let value: Complex = 1i; diff --git a/tests/functional/programs/001_math/041_number_bitwise_error.ndc b/tests/functional/programs/001_math/041_number_bitwise_error.ndc new file mode 100644 index 00000000..63480c7a --- /dev/null +++ b/tests/functional/programs/001_math/041_number_bitwise_error.ndc @@ -0,0 +1,2 @@ +// expect-error: No function called +print(1n & 2n); diff --git a/tests/functional/programs/001_math/042_int_min_remainder.ndc b/tests/functional/programs/001_math/042_int_min_remainder.ndc new file mode 100644 index 00000000..191e8629 --- /dev/null +++ b/tests/functional/programs/001_math/042_int_min_remainder.ndc @@ -0,0 +1,12 @@ +// A remainder stays representable even when the quotient it implies does not: +// `i64::MIN % -1` is 0 while `i64::MIN / -1` overflows. The checked remainder +// reports that quotient overflow, so `%` and `%%` wrap instead of failing. +let min = -9223372036854775807 - 1; +assert_eq(min % -1, 0); +assert_eq(min %% -1, 0); + +// ordinary remainder semantics are unchanged +assert_eq(-7 % 3, -1); +assert_eq(-7 %% 3, 2); +assert_eq(7 % -3, 1); +assert_eq(7 %% -3, 1); diff --git a/tests/functional/programs/001_math/043_int_min_division_error.ndc b/tests/functional/programs/001_math/043_int_min_division_error.ndc new file mode 100644 index 00000000..42403b46 --- /dev/null +++ b/tests/functional/programs/001_math/043_int_min_division_error.ndc @@ -0,0 +1,5 @@ +// Unlike the remainder, the quotient `i64::MIN / -1` is `2^63` and does not +// fit in an Int, so division still reports the overflow. +// expect-error: integer operation overflowed +let min = -9223372036854775807 - 1; +print(min / -1); diff --git a/tests/functional/programs/001_math/044_int_shift_overflow.ndc b/tests/functional/programs/001_math/044_int_shift_overflow.ndc new file mode 100644 index 00000000..726f8e6d --- /dev/null +++ b/tests/functional/programs/001_math/044_int_shift_overflow.ndc @@ -0,0 +1,5 @@ +// `checked_shl` only rejects an out-of-range shift amount, so `1 << 63` used +// to wrap to a negative Int silently while every other Int operation reported +// its overflow. +// expect-error: integer shift overflowed +print(1 << 63); diff --git a/tests/functional/programs/001_math/045_int_shift_in_range.ndc b/tests/functional/programs/001_math/045_int_shift_in_range.ndc new file mode 100644 index 00000000..58114687 --- /dev/null +++ b/tests/functional/programs/001_math/045_int_shift_in_range.ndc @@ -0,0 +1,6 @@ +// Shifts that fit are unaffected, including the two edges: `-1 << 63` is +// exactly i64::MIN, and zero survives any amount. +assert_eq(1 << 62, 4611686018427387904); +assert_eq(-1 << 63, -9223372036854775807 - 1); +assert_eq(0 << 64, 0); +assert_eq(8 >> 2, 2); diff --git a/tests/functional/programs/001_math/046_exact_zero_negative_power.ndc b/tests/functional/programs/001_math/046_exact_zero_negative_power.ndc new file mode 100644 index 00000000..c5b04cbd --- /dev/null +++ b/tests/functional/programs/001_math/046_exact_zero_negative_power.ndc @@ -0,0 +1,4 @@ +// `0 ^ negative` is division by zero for every exact base. The rational base +// used to reach num-rational directly and panic the whole process. +// expect-error: division by zero +print((0n/1n) ^ -1n); diff --git a/tests/functional/programs/001_math/047_non_finite_exponent.ndc b/tests/functional/programs/001_math/047_non_finite_exponent.ndc new file mode 100644 index 00000000..63d37ea4 --- /dev/null +++ b/tests/functional/programs/001_math/047_non_finite_exponent.ndc @@ -0,0 +1,10 @@ +// `fract()` is NaN for a non-finite exponent, which compares unequal to zero +// and used to send a negative base into the complex plane. Infinite exponents +// have real answers and must stay on the real path. +let inf = Number(1.0 / 0.0); +assert_eq((-2n) ^ inf, Number(1.0 / 0.0)); +assert_eq((-2n) ^ Number(-1.0 / 0.0), 0.0n); + +// a genuinely fractional exponent still escapes to the complex plane +assert(imag((-8n) ^ 0.5n) != 0.0n); +assert_eq((-8n) ^ 2n, 64n); diff --git a/tests/functional/programs/001_math/048_exact_results_stay_integers.ndc b/tests/functional/programs/001_math/048_exact_results_stay_integers.ndc new file mode 100644 index 00000000..6aacee7d --- /dev/null +++ b/tests/functional/programs/001_math/048_exact_results_stay_integers.ndc @@ -0,0 +1,22 @@ +// Division and the remainders build their result as a fraction. A denominator +// of one has to collapse back to an integer, or the value prints as `2` while +// no longer matching the integer arm any consumer switches on. +assert_eq(json_encode(4n / 2n), "2"); +assert_eq(json_encode(7n %% 3n), "1"); +assert_eq(json_encode(7n % 3n), "1"); +assert_eq(json_encode(2n ^ 2n), "4"); + +// genuine fractions are untouched +assert_eq(4n / 3n, 4n / 3n); +assert_eq(denominator(4n / 3n), 3n); + +// A negative power reaches the same normalization: the reciprocal of a whole +// number can itself be whole, and the rational-base branches likewise. +assert_eq(json_encode(1n ^ -1n), "1"); +assert_eq(json_encode((1n/2n) ^ -1n), "2"); +assert_eq(json_encode((1n/2n) ^ -2n), "4"); +assert_eq(json_encode(abs(-4n/2n)), "2"); + +// a reciprocal that is genuinely fractional keeps its exact form +assert_eq(2n ^ -1n, 1n/2n); +assert_eq(denominator(2n ^ -1n), 2n); diff --git a/tests/functional/programs/001_math/049_numeric_key_identity.ndc b/tests/functional/programs/001_math/049_numeric_key_identity.ndc new file mode 100644 index 00000000..148f3caa --- /dev/null +++ b/tests/functional/programs/001_math/049_numeric_key_identity.ndc @@ -0,0 +1,24 @@ +// Hashing takes a fast path for values an f64 represents exactly. Equal +// values must land in the same bucket whichever mode holds them, and unequal +// ones must not be merged by the shortcut. +let dyadic = %{}; +dyadic[0.5] = "float"; +dyadic[1n/2n] = "rational"; +assert_eq(dyadic.len(), 1); + +let inexact = %{}; +inexact[1n/3n] = "exact"; +inexact[1.0/3.0] = "approx"; +assert_eq(inexact.len(), 2); + +let integers = %{}; +integers[5] = "int"; +integers[5.0] = "float"; +integers[5n] = "number"; +assert_eq(integers.len(), 1); + +// a big integer that is still exactly an f64 agrees with that float +let big = %{}; +big[2n ^ 100n] = "bigint"; +big[Number(2.0 ^ 100)] = "float"; +assert_eq(big.len(), 1); diff --git a/tests/functional/programs/004_basic/039_binary_literal.ndc b/tests/functional/programs/004_basic/039_binary_literal.ndc index 932c59f9..387c1fac 100644 --- a/tests/functional/programs/004_basic/039_binary_literal.ndc +++ b/tests/functional/programs/004_basic/039_binary_literal.ndc @@ -1,7 +1,6 @@ // Simple binary number assert(0b1111 == 15); // Really big binary numbers are valid too! -assert(0b1111111111111111111111111111111111111111111111111111111111111111 == 18446744073709551615); +assert(0b1111111111111111111111111111111111111111111111111111111111111111n == 18446744073709551615n); // Underscores are allowed assert(0b10_10_10 == 42); - diff --git a/tests/functional/programs/004_basic/040_hex_literal.ndc b/tests/functional/programs/004_basic/040_hex_literal.ndc index f3a41ef1..205a7134 100644 --- a/tests/functional/programs/004_basic/040_hex_literal.ndc +++ b/tests/functional/programs/004_basic/040_hex_literal.ndc @@ -1,6 +1,5 @@ assert(0xffff == 65535); // Big numbers -assert(0xDEADDEADDEADDEADDEAD == 1051572691647698727460525); +assert(0xDEADDEADDEADDEADDEADn == 1051572691647698727460525n); // Underscores are allowed assert(0xf_Ff_dead == 268426925); - diff --git a/tests/functional/programs/004_basic/046_annotated_let_binding.ndc b/tests/functional/programs/004_basic/046_annotated_let_binding.ndc index b137a62f..9f2901d1 100644 --- a/tests/functional/programs/004_basic/046_annotated_let_binding.ndc +++ b/tests/functional/programs/004_basic/046_annotated_let_binding.ndc @@ -6,9 +6,9 @@ while false { let bool_value: Bool = true; let int_value: Int = 3; let float_value: Float = 3.0; -let rational_value: Number = 3 / 4; +let rational_value: Number = 3n / 4n; let complex_value: Number = 1 + 2i; -let number_value: Number = 3; +let number_value: Number = 3n; let string_value: String = "hello"; let option_value: Option = Some(3); diff --git a/tests/functional/programs/004_basic/051_annotated_let_subtype_accepted.ndc b/tests/functional/programs/004_basic/051_annotated_let_subtype_accepted.ndc index 7a231d3a..4468f962 100644 --- a/tests/functional/programs/004_basic/051_annotated_let_subtype_accepted.ndc +++ b/tests/functional/programs/004_basic/051_annotated_let_subtype_accepted.ndc @@ -1,3 +1,3 @@ // expect-output: 42 -let x: Number = 42; +let x: Number = 42n; print(x); diff --git a/tests/functional/programs/004_basic/055_annotated_let_op_assign_rejected.ndc b/tests/functional/programs/004_basic/055_annotated_let_op_assign_rejected.ndc index 88cbc43e..23234b5f 100644 --- a/tests/functional/programs/004_basic/055_annotated_let_op_assign_rejected.ndc +++ b/tests/functional/programs/004_basic/055_annotated_let_op_assign_rejected.ndc @@ -1,3 +1,3 @@ let x: Int = 3; x /= 4; -// expect-error: mismatched types +assert_eq(x, 0); diff --git a/tests/functional/programs/004_basic/058_annotated_list_index_op_assignment.ndc b/tests/functional/programs/004_basic/058_annotated_list_index_op_assignment.ndc index c0bf7d58..8d927b7b 100644 --- a/tests/functional/programs/004_basic/058_annotated_list_index_op_assignment.ndc +++ b/tests/functional/programs/004_basic/058_annotated_list_index_op_assignment.ndc @@ -1,3 +1,3 @@ -// expect-error: mismatched types: found Number but expected Int +// expect-error: mismatched types: found Float but expected Int let values: List = [1]; values[0] += 0.5; diff --git a/tests/functional/programs/004_basic/059_annotated_map_index_op_assignment.ndc b/tests/functional/programs/004_basic/059_annotated_map_index_op_assignment.ndc index 26554cd8..93710417 100644 --- a/tests/functional/programs/004_basic/059_annotated_map_index_op_assignment.ndc +++ b/tests/functional/programs/004_basic/059_annotated_map_index_op_assignment.ndc @@ -1,3 +1,3 @@ -// expect-error: mismatched types: found Number but expected Int +// expect-error: mismatched types: found Float but expected Int let values: Map = %{"one": 1}; values["one"] += 0.5; diff --git a/tests/functional/programs/004_basic/062_non_widenable_index_op_assignment.ndc b/tests/functional/programs/004_basic/062_non_widenable_index_op_assignment.ndc index df42dd15..891136d2 100644 --- a/tests/functional/programs/004_basic/062_non_widenable_index_op_assignment.ndc +++ b/tests/functional/programs/004_basic/062_non_widenable_index_op_assignment.ndc @@ -1,3 +1,3 @@ -// expect-error: mismatched types: found Number but expected Int +// expect-error: mismatched types: found Float but expected Int fn values() -> List => [1]; values()[0] += 0.5; diff --git a/tests/functional/programs/008_iterators/002_range_contains.ndc b/tests/functional/programs/008_iterators/002_range_contains.ndc index 26fd1972..74da3c79 100644 --- a/tests/functional/programs/008_iterators/002_range_contains.ndc +++ b/tests/functional/programs/008_iterators/002_range_contains.ndc @@ -39,4 +39,4 @@ assert_eq(0 in (-1..1), true); // 0 is within the range -1 to 1 assert_eq(1 in (-1..1), false); // 1 is outside the exclusive range // Edge case: infinite ranges -assert_eq(-9223372036854775808 in (5..), false); // Out of open range's bounds +assert_eq((-9223372036854775807 - 1) in (5..), false); // Out of open range's bounds diff --git a/tests/functional/programs/008_iterators/007_range_bound_types.ndc b/tests/functional/programs/008_iterators/007_range_bound_types.ndc new file mode 100644 index 00000000..7cf89390 --- /dev/null +++ b/tests/functional/programs/008_iterators/007_range_bound_types.ndc @@ -0,0 +1,4 @@ +// Every non-Int bound used to claim the integer was too large. Only an +// integer that genuinely does not fit is a size problem. +// expect-error: range bounds must be Int, got Number +for i in 0n..3n { print(i); } diff --git a/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc b/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc index 1824cb71..c5c37904 100644 --- a/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc +++ b/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc @@ -3,30 +3,30 @@ // MinHeap with BigInts (values outside i64 range) let h = MinHeap(); -h.push(9223372036854775808); // i64::MAX + 1 -h.push(9223372036854775809); +h.push(9223372036854775808n); // i64::MAX + 1 +h.push(9223372036854775809n); h.push(9223372036854775807); // i64::MAX (fits in i64) assert_eq(h.pop, 9223372036854775807); -assert_eq(h.pop, 9223372036854775808); -assert_eq(h.pop, 9223372036854775809); +assert_eq(h.pop, 9223372036854775808n); +assert_eq(h.pop, 9223372036854775809n); // MaxHeap with Rationals let h = MaxHeap(); -h.push(5 / 3); -h.push(7 / 4); -h.push(1 / 2); -assert_eq(h.pop, 7 / 4); -assert_eq(h.pop, 5 / 3); -assert_eq(h.pop, 1 / 2); +h.push(5n / 3n); +h.push(7n / 4n); +h.push(1n / 2n); +assert_eq(h.pop, 7n / 4n); +assert_eq(h.pop, 5n / 3n); +assert_eq(h.pop, 1n / 2n); // MinHeap: mixed int and Rational let h = MinHeap(); h.push(2); -h.push(3 / 2); // 1.5 -h.push(5 / 2); // 2.5 -assert_eq(h.pop, 3 / 2); +h.push(3n / 2n); // 1.5 +h.push(5n / 2n); // 2.5 +assert_eq(h.pop, 3n / 2n); assert_eq(h.pop, 2); -assert_eq(h.pop, 5 / 2); +assert_eq(h.pop, 5n / 2n); // MinHeap with Complex (ordered by (re, im) tuple, same as interpreter) let h = MinHeap(); diff --git a/tests/functional/programs/013_vector_math/009_vector_exact_match_precision.ndc b/tests/functional/programs/013_vector_math/009_vector_exact_match_precision.ndc index d9b003cd..d0cdd9c1 100644 --- a/tests/functional/programs/013_vector_math/009_vector_exact_match_precision.ndc +++ b/tests/functional/programs/013_vector_math/009_vector_exact_match_precision.ndc @@ -15,11 +15,11 @@ assert_eq(f, (14, 26)); // Any args fall to LUB (no scalar `-(Any, Any)` exists, so vec dispatch // can't pin a single overload at compile time): `Tuple - -// Tuple` infers as `Tuple`, the LUB across -// every numeric overload's return type. +// Tuple` remains `Tuple` because Int, Float, and Number +// are siblings and their least upper bound is Any. let l: List = [1, 2]; let p: Tuple = (l.first, l.first); let q: Tuple = (l.last, l.last); -let r: Tuple = p - q; -let s: Tuple = r * r; +let r: Tuple = p - q; +let s: Tuple = r * r; assert_eq(s, (1, 1)); diff --git a/tests/functional/programs/015_struct/017_struct_in_container.ndc b/tests/functional/programs/015_struct/017_struct_in_container.ndc index e0ac45d4..7c0d0895 100644 --- a/tests/functional/programs/015_struct/017_struct_in_container.ndc +++ b/tests/functional/programs/015_struct/017_struct_in_container.ndc @@ -1,6 +1,12 @@ struct Point { x: Int, y: Int } -fn total(points: List) => points.map(x).sum() +fn total(points: List) { + let total = 0; + for point in points { + total += point.x; + } + total +} let points: List = [Point(1, 2), Point(3, 4)]; print(total(points)) diff --git a/tests/functional/programs/016_casts/007_numeric_hierarchy.ndc b/tests/functional/programs/016_casts/007_numeric_hierarchy.ndc index fb5d54a1..8f0a91a3 100644 --- a/tests/functional/programs/016_casts/007_numeric_hierarchy.ndc +++ b/tests/functional/programs/016_casts/007_numeric_hierarchy.ndc @@ -1,7 +1,11 @@ -// Casts that depend on the numeric subtype hierarchy. Isolated here on -// purpose: the explicit-numeric-modes work makes Int, Float, and Number -// siblings, so this file is expected to change when that lands. -assert_eq(1 as Number, 1); -assert_eq(1.5 as Number, 1.5); -let value: Any = 1; -assert_eq(value as Int, 1); +// Int, Float, and Number are sibling types, so a cast between them can never +// succeed — `Number()`, `int()`, and `float()` are the conversions. A cast +// only recovers a numeric type that is hidden behind Any. +let integer: Any = 1; +assert_eq(integer as Int, 1); + +let float: Any = 1.5; +assert_eq(float as Float, 1.5); + +let advanced: Any = 1n; +assert_eq(advanced as Number, 1n); diff --git a/tests/functional/programs/016_casts/023_numeric_sibling_cast_error.ndc b/tests/functional/programs/016_casts/023_numeric_sibling_cast_error.ndc new file mode 100644 index 00000000..da4deb7a --- /dev/null +++ b/tests/functional/programs/016_casts/023_numeric_sibling_cast_error.ndc @@ -0,0 +1,4 @@ +// A cast asserts, it does not convert, and Int is not a Number under the +// sibling numeric model — `Number(1)` is the conversion. +// expect-error: invalid cast: Int can never be Number +1 as Number diff --git a/tests/functional/programs/016_casts/024_cast_selects_typed_overload.ndc b/tests/functional/programs/016_casts/024_cast_selects_typed_overload.ndc new file mode 100644 index 00000000..48b36fb8 --- /dev/null +++ b/tests/functional/programs/016_casts/024_cast_selects_typed_overload.ndc @@ -0,0 +1,18 @@ +// A cast is what lets a typed container overload be selected. `sum` has +// separate Int, Float, and Number overloads, and a value the analyser only +// knows as List matches none of them, so each cast below picks a +// different overload at compile time. +let values = %{}; +values.insert(1); +values.insert(2); +assert_eq((values.keys as List).sum(), 3); + +let floats: List = [1.0, 2.0, 3.0]; +assert_eq((floats as List).sum(), 6.0); + +let numbers: List = [1n, 2n, 3n]; +assert_eq((numbers as List).sum(), 6n); + +// An empty container conforms vacuously, so the overload still resolves. +let empty: List = []; +assert_eq((empty as List).sum(), 0); diff --git a/tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc b/tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc index 1e385a53..22f5dffd 100644 --- a/tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc +++ b/tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc @@ -8,7 +8,3 @@ assert_eq(list.index?(-3), Some(10)); // out of bounds in either direction is None assert_eq(list.index?(3), None); assert_eq(list.index?(-4), None); - -// an index too large to address the list is also None (no error) -assert_eq(list.index?(10^40), None); -assert_eq(list.index?(-(10^40)), None); diff --git a/tests/functional/programs/601_stdlib_list/009_list_get_too_large_error.ndc b/tests/functional/programs/601_stdlib_list/009_list_get_too_large_error.ndc index d30867d8..87dc75ee 100644 --- a/tests/functional/programs/601_stdlib_list/009_list_get_too_large_error.ndc +++ b/tests/functional/programs/601_stdlib_list/009_list_get_too_large_error.ndc @@ -1,3 +1,3 @@ -// an index that does not fit a usize is a conversion error -// expect-error: out of range -[10, 20, 30].get?(10^40); +// Number is deliberately not an index type. +// expect-error: No function called +[10, 20, 30].get?(10n^40n); diff --git a/tests/functional/programs/603_stdlib_seq/013_comparator_number_results.ndc b/tests/functional/programs/603_stdlib_seq/013_comparator_number_results.ndc new file mode 100644 index 00000000..4da43368 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/013_comparator_number_results.ndc @@ -0,0 +1,17 @@ +// Comparators may return any real Number. A Number backed by a float used to +// fall through to the "not a number" arm, so sorting Numbers by subtraction +// failed even though the same comparator worked on plain Floats. +let xs = [1.5n, 0.5n, 2.5n]; +xs.sort_by(fn(a, b) => a - b); +assert_eq(xs, [0.5n, 1.5n, 2.5n]); + +assert_eq([1.5n, 0.5n].min_by(fn(a, b) => a - b), 0.5n); +assert_eq([1.5n, 0.5n].max_by(fn(a, b) => a - b), 1.5n); + +// exact integer and rational results keep working +assert_eq([3n, 1n, 2n].min_by(fn(a, b) => a - b), 1n); +assert_eq([3n, 1n, 2n].min_by(fn(a, b) => (a - b) / 2n), 1n); + +// plain Int and Float comparators are unaffected +assert_eq([3, 1, 2].min_by(fn(a, b) => a - b), 1); +assert_eq([3.5, 1.5].min_by(fn(a, b) => a - b), 1.5); diff --git a/tests/functional/programs/603_stdlib_seq/014_comparator_complex_error.ndc b/tests/functional/programs/603_stdlib_seq/014_comparator_complex_error.ndc new file mode 100644 index 00000000..30052234 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/014_comparator_complex_error.ndc @@ -0,0 +1,4 @@ +// A complex result has no ordering, so it is still rejected — but the message +// now says which part of "number" the comparator failed. +// expect-error: comparator must return a real number +[3, 1].min_by(fn(a, b) => (a - b) * 1i); diff --git a/tests/functional/programs/604_stdlib_math/001_sum.ndc b/tests/functional/programs/604_stdlib_math/001_sum.ndc index 43e8cf18..c5b6af0f 100644 --- a/tests/functional/programs/604_stdlib_math/001_sum.ndc +++ b/tests/functional/programs/604_stdlib_math/001_sum.ndc @@ -1,5 +1,13 @@ assert_eq((1..10).sum(), 45); -assert_eq(%{1,2,3}.keys().sum(), 6); -assert_eq(%{1: "foo", 2: "bar", 3: "baz"}.keys().sum(), 6); assert_eq((1,2,3,4).sum(), 10); assert_eq([1,2,3,4,5].sum(), 15); +assert_eq([1.0,2.0,3.0].sum(), 6.0); +assert_eq([1n,2n,3n].sum(), 6n); + +// `Deque()` starts out as Deque and pushing does not narrow it, so the +// cast is what lets `sum` resolve against its Int overload. +let deque = Deque(); +deque.push_back(1); +deque.push_back(2); +deque.push_back(3); +assert_eq((deque as Deque).sum(), 6); diff --git a/tests/functional/programs/604_stdlib_math/002_product.ndc b/tests/functional/programs/604_stdlib_math/002_product.ndc index 6ac1cbbc..093b73ed 100644 --- a/tests/functional/programs/604_stdlib_math/002_product.ndc +++ b/tests/functional/programs/604_stdlib_math/002_product.ndc @@ -1,5 +1,5 @@ assert_eq((1..10).product(), 362880); -assert_eq(%{1,2,3}.keys().product(), 6); -assert_eq(%{1: "foo", 2: "bar", 3: "baz"}.keys().product(), 6); assert_eq((1,2,3,4).product(), 24); assert_eq([1,2,3,4,5].product(), 120); +assert_eq([1.0,2.0,3.0].product(), 6.0); +assert_eq([1n,2n,3n].product(), 6n); diff --git a/tests/functional/programs/604_stdlib_math/003_mixed_sum_error.ndc b/tests/functional/programs/604_stdlib_math/003_mixed_sum_error.ndc new file mode 100644 index 00000000..797c2816 --- /dev/null +++ b/tests/functional/programs/604_stdlib_math/003_mixed_sum_error.ndc @@ -0,0 +1,3 @@ +// Int, Float, and Number are siblings, so a heterogeneous list is List. +// expect-error: No function called 'sum' +print([1, 2.0].sum()); diff --git a/tests/functional/programs/605_stdlib_serde/003_numbers.ndc b/tests/functional/programs/605_stdlib_serde/003_numbers.ndc index b00fb82b..f82330f0 100644 --- a/tests/functional/programs/605_stdlib_serde/003_numbers.ndc +++ b/tests/functional/programs/605_stdlib_serde/003_numbers.ndc @@ -1,7 +1,7 @@ // Big integers round-trip exactly instead of degrading to floats -let big = 2 ^ 100 + 1; +let big = 2n ^ 100n + 1n; assert_eq(json_decode(json_encode(big)), big); -assert_eq(json_decode("123456789123456789123456789"), 123456789123456789123456789); +assert_eq(json_decode("123456789123456789123456789"), 123456789123456789123456789n); // Numbers written with a decimal point or exponent decode to floats assert_eq(json_decode("1e2"), 100.0); diff --git a/tests/functional/programs/605_stdlib_serde/004_lossy.ndc b/tests/functional/programs/605_stdlib_serde/004_lossy.ndc index 7850aae1..e0f76a49 100644 --- a/tests/functional/programs/605_stdlib_serde/004_lossy.ndc +++ b/tests/functional/programs/605_stdlib_serde/004_lossy.ndc @@ -2,7 +2,7 @@ // and cyclic values) by degrading it to the nearest JSON representation // Rationals become floats -assert_eq(json_encode_lossy(10/3), "3.3333333333333335"); +assert_eq(json_encode_lossy(10n/3n), "3.3333333333333335"); // Complex numbers become strings assert_eq(json_encode_lossy(5.0 + 3.1j), "\"5+3.1i\""); diff --git a/tests/functional/programs/605_stdlib_serde/010_encode_error_rational.ndc b/tests/functional/programs/605_stdlib_serde/010_encode_error_rational.ndc index 73020fdf..4c9a1a13 100644 --- a/tests/functional/programs/605_stdlib_serde/010_encode_error_rational.ndc +++ b/tests/functional/programs/605_stdlib_serde/010_encode_error_rational.ndc @@ -1,2 +1,2 @@ -json_encode(10/3); +json_encode(10n/3n); // expect-error: cannot convert a rational number to JSON diff --git a/tests/functional/programs/606_stdlib_rand/001_randf_int_bounds.ndc b/tests/functional/programs/606_stdlib_rand/001_randf_int_bounds.ndc new file mode 100644 index 00000000..0142fc15 --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/001_randf_int_bounds.ndc @@ -0,0 +1,10 @@ +// randf accepts Int bounds and always returns a Float +for _ in 0..100 { + let x: Float = randf(-1, 1); + assert(x >= -1.0 and x < 1.0); +} + +for _ in 0..100 { + let x: Float = randf(5); + assert(x >= 0.0 and x < 5.0); +} diff --git a/tests/functional/programs/606_stdlib_rand/002_randf_float_bounds.ndc b/tests/functional/programs/606_stdlib_rand/002_randf_float_bounds.ndc new file mode 100644 index 00000000..4f3110b4 --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/002_randf_float_bounds.ndc @@ -0,0 +1,15 @@ +// randf accepts Float bounds +for _ in 0..100 { + let x: Float = randf(); + assert(x >= 0.0 and x < 1.0); +} + +for _ in 0..100 { + let x: Float = randf(0.5); + assert(x >= 0.0 and x < 0.5); +} + +for _ in 0..100 { + let x: Float = randf(-2.5, 2.5); + assert(x >= -2.5 and x < 2.5); +} diff --git a/tests/functional/programs/606_stdlib_rand/003_randf_mixed_bounds.ndc b/tests/functional/programs/606_stdlib_rand/003_randf_mixed_bounds.ndc new file mode 100644 index 00000000..6995b3d4 --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/003_randf_mixed_bounds.ndc @@ -0,0 +1,20 @@ +// randf accepts any mix of Int, Float, and Number bounds +for _ in 0..100 { + let x: Float = randf(-1, 1.0); + assert(x >= -1.0 and x < 1.0); +} + +for _ in 0..100 { + let x: Float = randf(-1.0, 1); + assert(x >= -1.0 and x < 1.0); +} + +for _ in 0..100 { + let x: Float = randf(-1n, 1); + assert(x >= -1.0 and x < 1.0); +} + +for _ in 0..100 { + let x: Float = randf(-1.0, 1n); + assert(x >= -1.0 and x < 1.0); +} diff --git a/tests/functional/programs/606_stdlib_rand/004_randf_number_bounds.ndc b/tests/functional/programs/606_stdlib_rand/004_randf_number_bounds.ndc new file mode 100644 index 00000000..90653f72 --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/004_randf_number_bounds.ndc @@ -0,0 +1,15 @@ +// randf accepts Number bounds, including rationals +for _ in 0..100 { + let x: Float = randf(2n); + assert(x >= 0.0 and x < 2.0); +} + +for _ in 0..100 { + let x: Float = randf(-1n, 1n); + assert(x >= -1.0 and x < 1.0); +} + +for _ in 0..100 { + let x: Float = randf(1n / 4n, 1n / 2n); + assert(x >= 0.25 and x < 0.5); +} diff --git a/tests/functional/programs/606_stdlib_rand/005_randi_bounds.ndc b/tests/functional/programs/606_stdlib_rand/005_randi_bounds.ndc new file mode 100644 index 00000000..2533b974 --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/005_randi_bounds.ndc @@ -0,0 +1,16 @@ +// randi accepts Int bounds and always returns an Int +let a: Int = randi(); +assert(a >= 0); + +for _ in 0..100 { + let x: Int = randi(10); + assert(x >= 0 and x < 10); +} + +for _ in 0..100 { + let x: Int = randi(-5, 5); + assert(x >= -5 and x < 5); +} + +// a single-value range is deterministic +assert_eq(randi(5, 6), 5); diff --git a/tests/functional/programs/606_stdlib_rand/006_randi_number_bounds_error.ndc b/tests/functional/programs/606_stdlib_rand/006_randi_number_bounds_error.ndc new file mode 100644 index 00000000..18e5bbdb --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/006_randi_number_bounds_error.ndc @@ -0,0 +1,4 @@ +// randi takes Int bounds only. Number bounds are rejected when the call is +// resolved rather than converted, so `10n` never silently loses precision. +// expect-error: No function called 'randi' +randi(10n); diff --git a/tests/functional/programs/606_stdlib_rand/007_randf_invalid_bounds_error.ndc b/tests/functional/programs/606_stdlib_rand/007_randf_invalid_bounds_error.ndc new file mode 100644 index 00000000..e5a9acac --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/007_randf_invalid_bounds_error.ndc @@ -0,0 +1,2 @@ +// expect-error: cannot be greater than +randf(1, -1); diff --git a/tests/functional/programs/606_stdlib_rand/008_randi_float_bounds_error.ndc b/tests/functional/programs/606_stdlib_rand/008_randi_float_bounds_error.ndc new file mode 100644 index 00000000..dd5c956c --- /dev/null +++ b/tests/functional/programs/606_stdlib_rand/008_randi_float_bounds_error.ndc @@ -0,0 +1,3 @@ +// randi deliberately has no Float overloads: rounding direction would be ambiguous +// expect-error: No function called 'randi' +randi(0.5, 1.5); diff --git a/tests/functional/programs/900_bugs/bug0020_pow_huge_exponent.ndc b/tests/functional/programs/900_bugs/bug0020_pow_huge_exponent.ndc index 3974aa2e..59f1bedf 100644 --- a/tests/functional/programs/900_bugs/bug0020_pow_huge_exponent.ndc +++ b/tests/functional/programs/900_bugs/bug0020_pow_huge_exponent.ndc @@ -4,4 +4,4 @@ // instead of computing a result that wouldn't fit in any reasonable // amount of memory. // expect-error: exponent too large -print(-2 ^ 170141183460469231731687303715884105727) +print(-2n ^ 170141183460469231731687303715884105727n) diff --git a/tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc b/tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc index 1fbaa17a..9ecd0c23 100644 --- a/tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc +++ b/tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc @@ -4,4 +4,4 @@ // negative") and aborted the whole process. It now computes the // reciprocal, exactly like a plain negative integer exponent. // expect-output: 1/2 -print(2 ^ (1/-1)); +print(2n ^ (1n/-1n)); diff --git a/tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc b/tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc index 595baa86..1d97a0cc 100644 --- a/tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc +++ b/tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc @@ -1,6 +1,5 @@ // `abs(i64::MIN)` and `-(i64::MIN)` overflow the i64 range because the -// positive magnitude `2^63` does not fit in an i64. `Int::abs`/`Int::neg` -// used to evaluate `i.abs()`/`i.neg()` directly, panicking with "attempt to -// negate with overflow" in debug builds. They now promote to BigInt. -// expect-output: 9223372036854775808 +// positive magnitude `2^63` does not fit in an i64. Int arithmetic reports +// that checked overflow rather than promoting implicitly. +// expect-error: integer absolute value overflowed print(abs(-9223372036854775807 - 1)); diff --git a/tests/functional/programs/900_bugs/bug0033_cyclic_container_diagnostic.ndc b/tests/functional/programs/900_bugs/bug0033_cyclic_container_diagnostic.ndc new file mode 100644 index 00000000..aa7f0fe2 --- /dev/null +++ b/tests/functional/programs/900_bugs/bug0033_cyclic_container_diagnostic.ndc @@ -0,0 +1,7 @@ +// Formatting the "cannot compare" error walked the self-referential list +// forever and aborted the process with a stack overflow. The bounded +// description stops at a fixed depth instead. +// expect-error: cannot compare +let a = []; +a.push(a); +print(a < 1); diff --git a/tests/functional/programs/998_not_desired/big_int_ranges.ndc b/tests/functional/programs/998_not_desired/big_int_ranges.ndc index bca4ad05..75c9a00c 100644 --- a/tests/functional/programs/998_not_desired/big_int_ranges.ndc +++ b/tests/functional/programs/998_not_desired/big_int_ranges.ndc @@ -1,4 +1,4 @@ // expect-error: Integer too large for range bounds // Not sure what to think of this one, but right now you cannot construct ranges with ints outside of the i64 range // it does make type analysis a tiny bit easier though -(2^1024)..((2^1024)+1) +(2n^1024n)..((2n^1024n)+1n) diff --git a/tests/functional/programs/999_cursed/comical_factorial.ndc b/tests/functional/programs/999_cursed/comical_factorial.ndc index 790f4101..b27fc1bf 100644 --- a/tests/functional/programs/999_cursed/comical_factorial.ndc +++ b/tests/functional/programs/999_cursed/comical_factorial.ndc @@ -1,6 +1,6 @@ -let v, n = 100, 100; +let v, n = 100n, 100; while n > 1 { n -= 1; v *= n; } -assert_eq(v, 100.factorial); +assert_eq(v, 100n.factorial); diff --git a/tests/proptest/tests/panic.rs b/tests/proptest/tests/panic.rs index a6e50337..aa44a0d4 100644 --- a/tests/proptest/tests/panic.rs +++ b/tests/proptest/tests/panic.rs @@ -29,7 +29,7 @@ use ndc_analyser::{Analyser, ScopeTree}; use ndc_core::FunctionRegistry; use ndc_core::r#struct::StructRegistry; -use ndc_lexer::{Span, Token, TokenLocation}; +use ndc_lexer::{NumericLiteral, Span, Token, TokenLocation}; use ndc_parser::Parser; use ndc_vm::compiler::Compiler; use ndc_vm::value::{Function as VmFunction, Object as VmObject}; @@ -94,12 +94,11 @@ fn install_quiet_panic_hook() { } /// All unit-variant tokens, identifiers/strings/numbers from small pools, -/// and a couple of `BigInt`/`Complex` literals. Combined uniformly with a +/// and a couple of advanced-number/complex literals. Combined uniformly with a /// low-weight `OpAssign` wrapping an augmentable inner token. fn arb_token() -> impl Strategy { let mut atoms: Vec = vec![ // Literals (unit) - Token::Infinity, Token::True, Token::False, // Operators @@ -168,15 +167,24 @@ fn arb_token() -> impl Strategy { atoms.push(Token::String(s.to_string())); } for i in [-2_i64, -1, 0, 1, 2, 42, i64::MAX] { - atoms.push(Token::Int64(i)); + atoms.push(Token::NumericLiteral(NumericLiteral::Int64(i))); } for f in [0.0_f64, 1.0, -1.0, 0.5, f64::INFINITY, f64::NAN] { - atoms.push(Token::Float64(f)); + atoms.push(Token::NumericLiteral(NumericLiteral::Float64(f))); } - atoms.push(Token::BigInt(num::BigInt::from(0))); - atoms.push(Token::BigInt(num::BigInt::from(i128::MAX))); - atoms.push(Token::Complex(num::complex::Complex64::new(0.0, 1.0))); - atoms.push(Token::Complex(num::complex::Complex64::new(2.0, -3.0))); + atoms.push(Token::NumericLiteral(NumericLiteral::NumberInt( + num::BigInt::from(0), + ))); + atoms.push(Token::NumericLiteral(NumericLiteral::NumberInt( + num::BigInt::from(i128::MAX), + ))); + atoms.push(Token::NumericLiteral(NumericLiteral::NumberFloat(0.5))); + atoms.push(Token::NumericLiteral(NumericLiteral::Complex( + num::complex::Complex64::new(0.0, 1.0), + ))); + atoms.push(Token::NumericLiteral(NumericLiteral::Complex( + num::complex::Complex64::new(2.0, -3.0), + ))); // Inner token of an `OpAssign`. Lexer invariant: only augmentable tokens // appear here, so we mirror that to avoid finding "panics" that no real