Compare commits

..

33 Commits

Author SHA1 Message Date
d6b064efcd 🐐 Test files fix 2026-04-25 22:08:25 +02:00
d2d09c770d IASON 2026-04-25 22:03:30 +02:00
ff1c392dd6 🐐 Hash tables 2026-04-25 20:49:31 +02:00
c9fd245bb3 🐐 Moving docs 2026-04-25 20:33:03 +02:00
272881e6cf 🐐 Line numbers in errors 2026-04-25 20:21:16 +02:00
382492a6fc 🐐 MVTA, CRIBRA, CONFLA 2026-04-25 20:20:54 +02:00
5e4c7350a9 🐐 ORDINA with comparitor 2026-04-25 18:50:37 +02:00
8d06407527 🐐 Zipping 2026-04-25 18:34:47 +02:00
3a80c4e941 🐐 Array functions 2026-04-25 18:18:13 +02:00
d03af56e67 🐐 Better array definition 2026-04-24 19:28:21 +02:00
37fdff2db5 🐐 Tests 2026-04-24 19:13:48 +02:00
f197c2c3d5 🐐 Step in for loop 2026-04-24 18:33:48 +02:00
dbaf01b6a3 🐐 String uppercase/lowercase functions 2026-04-24 18:10:50 +02:00
37050e3e3b 🐐 Compiler fixes 2026-04-24 17:14:08 +02:00
6d4a456010 🐐 LITTERA 2026-04-24 16:45:23 +02:00
ea72c91870 🐐 More compound operators 2026-04-24 16:26:17 +02:00
76bf509d48 🐐 VSCode extension improvement 2026-04-24 16:11:45 +02:00
bdf72b2bcc 🐐 LTE/GTE 2026-04-24 16:02:03 +02:00
92301f3ff6 🐐 Snippets 2026-04-24 15:37:46 +02:00
27c5f7bf56 🐐 Index assignment 2026-04-22 16:10:11 +02:00
5418dfa577 🐐 PER deconstructing 2026-04-22 15:43:32 +02:00
60fe691731 🐐 Array concat 2026-04-22 15:35:51 +02:00
634c5a2f93 🐐 Arrays with newlines 2026-04-22 15:29:38 +02:00
60b45869fb 🐐 short circuit evaluation 2026-04-22 14:29:39 +02:00
461bfbbdc5 🐐 INVOCA error message 2026-04-22 14:09:01 +02:00
5e2ebcdc9d 🐐 Fixes 2026-04-22 13:45:55 +02:00
f5b8986681 🐐 Compiler fix 2026-04-22 13:24:55 +02:00
24b187c23c 🐐 Snippets 2026-04-22 13:07:50 +02:00
c55a63f46c 🐐 NVLLVUS fix 2026-04-22 12:35:00 +02:00
791ed2491e 🐐 NVMERVS 2026-04-22 12:24:11 +02:00
3af2115e7d 🐐 VSCode extension fixes 2026-04-22 11:52:18 +02:00
25e88a6362 🐐 SCINDE 2026-04-22 11:48:54 +02:00
0da0907a62 🐐 VSCode extension fixes 2026-04-22 11:44:32 +02:00
87 changed files with 6949 additions and 3698 deletions

574
DOCS.md Normal file
View File

@@ -0,0 +1,574 @@
# CENTVRION Language Reference
Full syntax documentation for the CENTVRION language. For the formal grammar, see [`language/main.pdf`](language/main.pdf). For installation and a minimal example, see [`README.md`](README.md).
## Example code
### Hello World
![Hello World](snippets/hello_world.png)
### Recursive Fibonacci number function
![Fibonacci](snippets/fibonacci.png)
### Number guessing game
![Number guessing game](snippets/guessing.png)
## Variables
Variables are set with the `DESIGNA` and `VT` keywords. Type is inferred.
![Variable assignment](snippets/variable.png)
Variable can consist of lower-case letters, numbers, as well as `_`.
### Compound assignment
`AVGE` (+=), `MINVE` (-=), `MVLTIPLICA` (*=) and `DIVIDE` (/=) are shorthand for updating a variable with an arithmetic operation:
![Compound assignment](snippets/compound.png)
```
> VIII
```
`x AVGE III` is equivalent to `DESIGNA x VT x + III`; `MINVE`, `MVLTIPLICA` and `DIVIDE` expand the same way with subtraction, multiplication and division.
### Destructuring
Multiple variables can be assigned at once by unpacking an array or multi-return function:
![Destructuring function](snippets/destructure_fn.png)
The number of targets must match the length of the array. This also works with array literals:
![Destructuring array](snippets/destructure_array.png)
## Data types
### NVLLVS
`NVLLVS` is a special kind of data type in `CENTVRION`, similar to the `null` value in many other languages. `NVLLVS` can be 0 if evaluated as an int or float, or an empty string if evaluated as a string. `NVLLVS` cannot be evaluated as a boolean.
### Strings
Strings are written as text in quotes (`'` or `"`).
![String literal](snippets/string_literal.png)
Strings are concatenated with `&`:
![String concatenation](snippets/string_concat.png)
`NVLLVS` coerces to an empty string when used with `&`. Note: `+` is for arithmetic only — using it on strings raises an error.
#### String Interpolation
Double-quoted strings support interpolation with `{expression}`:
![String interpolation](snippets/string_interp.png)
Any expression can appear inside `{}`. Values are coerced to strings the same way as with `&` (integers become Roman numerals, booleans become `VERITAS`/`FALSITAS`, etc.).
Single-quoted strings do **not** interpolate — `'{nomen}'` is the literal text `{nomen}`. Use `{{` and `}}` for literal braces in double-quoted strings: `"use {{braces}}"``use {braces}`.
#### String Indexing and Slicing
Strings support the same indexing and slicing syntax as arrays. Indexing is 1-based and returns a single-character string:
![String indexing](snippets/string_index.png)
```
> S
> L
```
Slicing uses `VSQVE` with inclusive bounds, returning a substring:
![String slicing](snippets/string_slice.png)
```
> ALV
```
Integer modulo is `RELIQVVM`: `VII RELIQVVM III` evaluates to `I`. Under the `FRACTIO` module it returns a fraction, so `IIIS RELIQVVM IS` is `S` (i.e. 1/2).
### Integers
Integers must be written in roman numerals using the following symbols:
|Symbol|Value|
|------|-----|
|`I`|1|
|`V`|5|
|`X`|10|
|`L`|50|
|`C`|100|
|`D`|500|
|`M`|1000|
Each of the symbols written by themself is equal to the value of the symbol. Different symbols written from largest to smallest are equal to the sum of the symbols. Two to three of the same symbol written consecutively is equal to the sum of those symbols (only true for `I`s, `X`s, `C`s or `M`s ). A single `I` written before a `V` or `X` is equal to 1 less than the value of the second symbol. Similarly, an `X` written before a `L` or `C` is 10 less than the second symbol, and a `C` written before a `D` or `M` is 100 less than the second symbol.
Because of the restrictions of roman numerals, numbers above 3.999 are impossible to write in the base `CENTVRION` syntax. If numbers of that size are required, see the `MAGNVM` module.
The number 0 can be expressed with the keyword `NVLLVS`.
#### Negative numbers
Negative numbers can be expressed as `NVLLVS` minus the value. For an explicit definition of negative numbers, see the `SVBNVLLA` module.
### Floats
The base `CENTVRION` syntax does not allow for floats. However, the `FRACTIO` module adds a syntax for fractions.
### Booleans
Booleans are denoted with the keywords `VERITAS` for true and `FALSITAS` for false.
### Arrays
Arrays are defined using square brackets (`[]`) and commas (`,`):
![Array literal](snippets/array_literal.png)
An array of integers can also be initialized with the `VSQVE` keyword. The range is inclusive on both ends:
![Array with VSQVE](snippets/array_vsqve.png)
```
> [I, II, III, IV, V, VI, VII, VIII, IX, X]
```
Individual elements can be accessed by index using square brackets. Indexing is 1-based, so `I` refers to the first element:
![Array indexing](snippets/array_index.png)
```
> I
```
Arrays are concatenated with `@`:
![Array concatenation](snippets/array_concat.png)
```
> [I, II, III, IV, V]
```
Both operands must be arrays — using `@` on non-arrays raises an error.
A sub-array can be extracted with `VSQVE` inside the index brackets. Both bounds are inclusive and 1-based:
![Array slicing](snippets/array_slice.png)
```
> [XX, XXX, XL]
```
### Dicts (TABVLA)
Dicts are key-value maps created with the `TABVLA` keyword and curly braces:
![Dict creation](snippets/dict_create.png)
Keys must be strings or integers. Values are accessed and assigned with square brackets:
![Dict access](snippets/dict_access.png)
Iterating over a dict with `PER` loops over its keys:
![Dict iteration](snippets/dict_per.png)
`LONGITVDO(dict)` returns the number of entries. `CLAVES(dict)` returns the keys as an array.
## Conditionals
### SI/TVNC
If-then statements are denoted with the keywords `SI` (if) and `TVNC` (then). Thus, the code
![SI/TVNC](snippets/si_tvnc.png)
Will return `I` (1), as the conditional evaluates `x` to be true.
### Boolean expressions
In conditionals, `EST` functions as an equality evaluation, `DISPAR` as not-equal, and `MINVS` (<), `PLVS` (>), `HAVD_PLVS` (≤), and `HAVD_MINVS` (≥) function as inequality evaluation.
### ALIVD
When using `SI`/`TVNC` statements, you can also use `ALIVD` as an "else".
![ALIVD](snippets/alivd.png)
```
> I
```
`SI` statements may follow immediately after `ALIVD`.
![ALIVD SI](snippets/alivd_si.png)
```
> II
```
### Boolean operators
The keyword `ET` can be used as a boolean "and". The keyword `AVT` can be used as a boolean "or".
![Boolean operators](snippets/boolean_ops.png)
```
> II
```
## Loops
### DONICVM loops
![DONICVM loop](snippets/donicvm.png)
```
> LV
```
An optional `GRADV` clause sets the stride. The step must be a nonzero
integer expression; positive values ascend, negative values descend, and
the endpoint is included only when the stride lands on it exactly.
![DONICVM with GRADV](snippets/donicvm_gradv.png)
```
> XXV
```
### DVM loops
![DVM loop](snippets/dvm.png)
```
> XI
```
### AETERNVM loops
`AETERNVM FAC { ... }` is shorthand for an infinite loop — equivalent
to `DVM FALSITAS FAC { ... }` but without relying on `DVM`'s inverted
condition. Exit the loop with `ERVMPE` (or `REDI` from inside a function).
![AETERNVM loop](snippets/aeternvm.png)
```
> X
```
### PER loops
![PER loop](snippets/per.png)
```
> I
> II
> III
> IV
> V
```
Variables can be unpacked in `PER` loops, similar to `DESIGNA` destructuring:
![PER destructuring](snippets/per_destructure.png)
```
> III
> VII
```
## Error handling
Errors can be caught using `TEMPTA` (temptare = to try) and `CAPE` (capere = to catch). The `CAPE` block binds the error message to a variable as a string.
![TEMPTA / CAPE](snippets/tempta_cape.png)
```
> Division by zero
```
If the try block succeeds, the catch block is skipped. If an error occurs in the catch block, it propagates up. `TEMPTA`/`CAPE` blocks can be nested.
## Functions
Functions are defined with the `DEFINI` and `VT` keywords. The `REDI` keyword is used to return. `REDI` can also be used to end the program, if used outside of a function.
Calling a function is done with the `INVOCA` keyword.
![Function definition](snippets/function.png)
```
> CXXI
```
## First-class functions
Functions are first-class values in CENTVRION. They can be assigned to variables, passed as arguments, returned from functions, and stored in arrays or dicts.
Anonymous functions are created with the `FVNCTIO` keyword:
![FVNCTIO](snippets/fvnctio.png)
```
> XIV
```
`INVOCA` accepts any expression as the callee, not just a name:
![INVOCA expressions](snippets/invoca_expr.png)
```
> VI
> VI
> XVI
```
Note: CENTVRION does **not** have closures. When a function is called, it receives a copy of the *caller's* scope, not the scope where it was defined. Variables from a function's definition site are only available if they also exist in the caller's scope at call time.
## Built-ins
### DIC
`DIC(value, ...)`
Prints one or more values to stdout, space-separated, with integers rendered as Roman numerals. Returns the printed string.
![DIC](snippets/dic.png)
### AVDI
`AVDI()`
Reads one line from stdin and returns it as a string.
### AVDI_NVMERVS
`AVDI_NVMERVS()`
Reads one line from stdin, parses it as a Roman numeral, and returns it as an integer. Raises an error if the input is not a valid numeral.
### CONTINVA
`CONTINVA`
Skips the rest of the current loop body and continues to the next iteration (`DVM` or `PER`). Has no meaningful return value.
### ERVMPE
`ERVMPE`
Breaks out of the current loop (`DVM` or `PER`). Has no meaningful return value.
### LONGITVDO
`LONGITVDO(array)`, `LONGITVDO(string)`, or `LONGITVDO(dict)`
Returns the length of `array` (element count), `string` (character count), or `dict` (entry count) as an integer.
### CLAVES
`CLAVES(dict)`
Returns the keys of `dict` as an array.
### ORDINA
`ORDINA(array)` or `ORDINA(array, comparator)`
Sorts an array. Returns a new sorted array; the original is unchanged.
Without a comparator, sorts in ascending order. All elements must be the same type — integers, fractions, or strings. Integers and fractions sort numerically; strings sort lexicographically.
With a comparator, the type-uniformity rule is dropped — the comparator decides ordering. The comparator must be a function of exactly two parameters and must return `VERAX`. `comparator(a, b)` returns `VERITAS` iff `a` should come **before** `b`; two elements are treated as equal when both `comparator(a, b)` and `comparator(b, a)` are `FALSITAS`.
![ORDINA with comparator](snippets/ordina_cmp.png)
```
> [V III II I]
```
### MVTA
`MVTA(array, fn)`
Returns a new array obtained by applying `fn` to every element of `array`. The original array is unchanged. `fn` must be a function of exactly one parameter; its return value is unrestricted, so `MVTA` may produce an array of a different element type than its input.
![MVTA doubling each element](snippets/mvta.png)
```
> [II IV VI VIII]
```
### CRIBRA
`CRIBRA(array, predicate)`
Returns a new array containing the elements of `array` for which `predicate` returns `VERITAS`, in their original order. The original array is unchanged. `predicate` must be a function of exactly one parameter and must return `VERAX`.
![CRIBRA keeping elements ≤ III](snippets/cribra.png)
```
> [I II III]
```
### CONFLA
`CONFLA(array, initial, fn)`
Left fold: starts with `initial` as the accumulator, then for each element `e` of `array` updates the accumulator to `fn(acc, e)`, and returns the final accumulator. The original array is unchanged. `fn` must be a function of exactly two parameters. If `array` is empty, `initial` is returned unchanged.
![CONFLA summing elements](snippets/confla.png)
```
> XVI
```
### ADDE
`ADDE(array, value)`
Returns a new array with `value` appended at the end. The original array is unchanged.
### TOLLE
`TOLLE(array, idx)`
Returns a new array with the element at 1-based position `idx` removed. The index must be an integer in the range `[I, LONGITVDO(array)]`; out-of-range indices raise an error.
### INSERE
`INSERE(array, idx, value)`
Returns a new array with `value` inserted at 1-based position `idx`, shifting later elements one position to the right. The index must be an integer in the range `[I, LONGITVDO(array) + I]`; passing `LONGITVDO(array) + I` is equivalent to `ADDE`.
### NECTE
`NECTE(array1, array2)`
Weaves two arrays together into a new array of two-element pair arrays. The two inputs must have equal length; mismatched lengths raise an error.
### IVNGE
`IVNGE(keys, values)`
Builds a dict by yoking two parallel arrays — the i-th element of `keys` becomes the key for the i-th element of `values`. The two arrays must have equal length. Keys must be strings or integers. If `keys` contains duplicates, the later value wins.
### SENATVS
`SENATVS(bool, ...)` or `SENATVS([bool])`
Returns VERITAS if a strict majority of the arguments are VERITAS, FALSITAS otherwise. Also accepts a single array of booleans. All values must be booleans. Ties return FALSITAS.
### NVMERVS
`NVMERVS(string)`
Parses a Roman numeral string and returns its integer value. The argument must be a string containing a valid Roman numeral. Respects the `MAGNVM` and `SVBNVLLA` modules for large and negative numbers respectively.
### TYPVS
`TYPVS(value)`
Returns the type of `value` as a string: `NVMERVS` (integer), `LITTERA` (string), `VERAX` (boolean), `CATALOGVS` (list), `FRACTIO` (fraction), `TABVLA` (dict), `FVNCTIO` (function), or `NVLLVS` (null).
### LITTERA
`LITTERA(value)`
Returns `value` formatted as the same display string `DIC` would print. Integers become Roman numerals (zero becomes `NVLLVS`), fractions use the `S`/`:`/`.`/`|` notation, booleans become `VERITAS`/`FALSITAS`, arrays are space-separated in brackets, and dicts use the `{ key VT value, ... }` form. Strings pass through unchanged. Respects `MAGNVM` and `SVBNVLLA` for large and negative numbers. Inverse of `NVMERVS` for integers: `NVMERVS(LITTERA(n)) == n`.
### DORMI
`DORMI(n)`
Sleeps for `n` seconds, where `n` is an integer, fraction, or NVLLVS (treated as 0). Returns nothing meaningful.
### QVAERE
`QVAERE(pattern, string)`
Returns an array of all non-overlapping matches of the regex `pattern` in `string`. Both arguments must be strings. Patterns use extended regular expression syntax with Roman numeral quantifiers (`{III}` for exactly 3, `{II,V}` for 25, `{III,}` for 3 or more). Returns an empty array if there are no matches. Raises an error if the pattern is invalid.
### SVBSTITVE
`SVBSTITVE(pattern, replacement, string)`
Replaces all non-overlapping matches of the regex `pattern` in `string` with `replacement`. All three arguments must be strings. The replacement string supports backreferences (`\I`, `\II`, etc.) to captured groups. Returns the resulting string. Raises an error if the pattern is invalid.
### SCINDE
`SCINDE(string, delimiter)`
Splits `string` by `delimiter` and returns an array of substrings. Both arguments must be strings. If the delimiter is not found, returns a single-element array containing the original string. If the delimiter is an empty string, splits into individual characters.
### MAIVSCVLA
`MAIVSCVLA(string)`
Returns a new string with every ASCII letter `a``z` replaced by its uppercase counterpart `A``Z`. All other bytes (digits, punctuation, non-ASCII) pass through unchanged.
### MINVSCVLA
`MINVSCVLA(string)`
Returns a new string with every ASCII letter `A``Z` replaced by its lowercase counterpart `a``z`. All other bytes (digits, punctuation, non-ASCII) pass through unchanged.
## Modules
Modules are additions to the base `CENTVRION` syntax. They add or change certain features. Modules are included in your code by having
![Module declaration](snippets/module_decl.png)
In the beginning of your source file.
Vnlike many other programming languages with modules, the modules in `CENTVRION` are not libraries that can be "imported" from other scripts written in the language. They are features of the compiler, disabled by default.
### FORS
![CVM FORS](snippets/fors.png)
The `FORS` module allows you to add randomness to your `CENTVRION` program. It adds 4 new built-in functions: `FORTVITVS_NVMERVS(int, int)`, `FORTVITA_ELECTIO(['a])`, `DECIMATIO(['a])`, and `SEMEN(int)`.
`FORTVITVS_NVMERVS(int, int)` picks a random int in the (inclusive) range of the two given ints.
`FORTVITA_ELECTIO(['a])` picks a random element from the given array. `FORTVITA_ELECTIO(array)` is identical to ```array[FORTVITVS_NVMERVS(I, LONGITVDO(array))]```.
`DECIMATIO(['a])` returns a copy of the given array with a random tenth of its elements removed. Arrays with fewer than 10 elements are returned unchanged.
`SEMEN(int)` seeds the random number generator for reproducibility.
### FRACTIO
![CVM FRACTIO](snippets/fractio.png)
The `FRACTIO` module adds floats, in the form of base 12 fractions.
In the `FRACTIO` module, `.` represents 1/12, `:` represents 1/6 and `S` represents 1/2. The symbols must be written from highest to lowest. So 3/4 would be written as "`S:.`".
Fractions can be written as an extension of integers. So 3.5 would be "`IIIS`".
The symbol `|` can be used to denote that the following fraction symbols are 1 "level down" in base 12. So after the first `|`, the fraction symbols denote 144ths instead of 12ths. So 7 and 100/144 would be "`VIIS:|::`", as "7 + 100/144" is also "7+8/12+4/144".
A single "set" of fraction symbols can only represent up to 11/12, as 12/12 can be written as 1.
### IASON
> ⚠ **Warning.** The `IASON` module enables your program to read and write non-Roman numerals. Numbers handled by `IASON_LEGE` and `IASON_SCRIBE` use the decimal digits `0``9` (e.g. `42`, `1789`, `30`), not Roman numerals. This goes against the design philosophy of CENTVRION and should not be used unless absolutely necessary.
![CVM IASON](snippets/iason.png)
The `IASON` module adds two builtins for converting between `CENTVRION` values and JSON strings.
`IASON_LEGE(string)` parses a JSON string and returns the corresponding `CENTVRION` value. Mappings: JSON `null` → `NVLLVS`, `true`/`false` → `VERITAS`/`FALSITAS`, integer → numeral, string → string, array → array, object → `TABVLA` (string keys).
JSON floats with no fractional part (e.g. `3.0`) come back as integers. Other floats depend on whether the `FRACTIO` module is also loaded: with `FRACTIO`, `0.1` parses to the exact fraction `I:|::|::|S:.|S.|:` (1/10); without it, the value is floored to the nearest integer.
![IASON_LEGE example](snippets/iason_lege.png)
```
> Marcus
> XXX
> [gladius scutum]
```
`IASON_SCRIBE(value)` serializes a `CENTVRION` value to a JSON string. Integers and fractions become JSON numbers (fractions via shortest-round-trip float), strings become JSON strings (with the standard escapes), arrays become arrays, dicts become objects (insertion order preserved). Functions and dicts with non-string keys raise an error.
![IASON_SCRIBE example](snippets/iason_scribe.png)
```
> {"nomen": "Marcus", "anni": 30}
```
### MAGNVM
![CVM MAGNVM](snippets/magnvm.png)
`MAGNVM` adds the ability to write integers larger than `MMMCMXCIX` (3.999) in your code, by adding the thousands operator, "`_`".
When `_` is added _after_ a numeric symbol, the symbol becomes 1.000 times larger. The operator can be added to the same symbol multiple times. So "`V_`" is 5.000, and "`V__`" is 5.000.000. The strict rules for integers still apply, so 4.999 cannot be written as "`IV_`", but must instead be written as "`MV_CMXCIX`".
All integer symbols except `I` may be given a `_`.
### SCRIPTA
![CVM SCRIPTA](snippets/scripta.png)
The `SCRIPTA` module adds file I/O to your `CENTVRION` program. It adds 3 new built-in functions: `LEGE`, `SCRIBE`, and `ADIVNGE`.
`LEGE(string)` reads the contents of the file at the given path and returns them as a string.
`SCRIBE(string, string)` writes the second argument to the file at the path given by the first argument, overwriting any existing content.
`ADIVNGE(string, string)` appends the second argument to the file at the path given by the first argument.
### RETE
![CVM RETE](snippets/rete.png)
The `RETE` module adds networking to your `CENTVRION` program.
`PETE(string)` performs an HTTP GET request to the given URL and returns the response body as a string.
`PETITVR(string, function)` registers a GET handler for the given path. The handler function receives a single argument: a dictionary with keys `"via"` (the request path), `"quaestio"` (query string), and `"methodus"` (HTTP method). The handler's return value becomes the response body (200 OK). Unmatched paths return a 404.
`AVSCVLTA(integer)` starts an HTTP server on the given port. This call blocks indefinitely, serving registered routes. Routes must be registered with `PETITVR` before calling `AVSCVLTA`. Ports above 3999 require the `MAGNVM` module.
### SVBNVLLA
![CVM SVBNVLLA](snippets/svbnvlla.png)
The `SVBNVLLA` module adds the ability to write negative numbers as `-II` instead of `NVLLVS-II`.

430
README.md
View File

@@ -1,431 +1,21 @@
# About
`CENTVRION` is the programming language for the modern roman.
# Documentation
## Hello World
## Example code
### Hello World
![Hello World](snippets/hello_world.png)
### Recursive Fibonacci number function
## Running
![Fibonacci](snippets/fibonacci.png)
### Number guessing game
![Number guessing game](snippets/guessing.png)
## Variables
Variables are set with the `DESIGNA` and `VT` keywords. Type is inferred.
![Variable assignment](snippets/variable.png)
Variable can consist of lower-case letters, numbers, as well as `_`.
### Compound assignment
`AVGE` (+=) and `MINVE` (-=) are shorthand for incrementing or decrementing a variable:
![Compound assignment](snippets/compound.png)
```
> VIII
```bash
./cent -i FILE.cent # interpret a .cent file
./cent -c FILE.cent # compile (not yet implemented)
```
`x AVGE III` is equivalent to `DESIGNA x VT x + III`.
Dependencies: `rply`, `docopt`. Install via `pip install rply docopt`.
### Destructuring
## Documentation
Multiple variables can be assigned at once by unpacking an array or multi-return function:
![Destructuring function](snippets/destructure_fn.png)
The number of targets must match the length of the array. This also works with array literals:
![Destructuring array](snippets/destructure_array.png)
## Data types
### NVLLVS
`NVLLVS` is a special kind of data type in `CENTVRION`, similar to the `null` value in many other languages. `NVLLVS` can be 0 if evaluated as an int or float, or an empty string if evaluated as a string. `NVLLVS` cannot be evaluated as a boolean.
### Strings
Strings are written as text in quotes (`'` or `"`).
![String literal](snippets/string_literal.png)
Strings are concatenated with `&`:
![String concatenation](snippets/string_concat.png)
`NVLLVS` coerces to an empty string when used with `&`. Note: `+` is for arithmetic only — using it on strings raises an error.
#### String Interpolation
Double-quoted strings support interpolation with `{expression}`:
![String interpolation](snippets/string_interp.png)
Any expression can appear inside `{}`. Values are coerced to strings the same way as with `&` (integers become Roman numerals, booleans become `VERITAS`/`FALSITAS`, etc.).
Single-quoted strings do **not** interpolate — `'{nomen}'` is the literal text `{nomen}`. Use `{{` and `}}` for literal braces in double-quoted strings: `"use {{braces}}"``use {braces}`.
#### String Indexing and Slicing
Strings support the same indexing and slicing syntax as arrays. Indexing is 1-based and returns a single-character string:
```
"SALVTE"[I] @> "S"
"SALVTE"[III] @> "L"
```
Slicing uses `VSQVE` with inclusive bounds, returning a substring:
```
"SALVTE"[II VSQVE IV] @> "ALV"
```
Integer modulo is `RELIQVVM`: `VII RELIQVVM III` evaluates to `I`. Under the `FRACTIO` module it returns a fraction, so `IIIS RELIQVVM IS` is `S` (i.e. 1/2).
### Integers
Integers must be written in roman numerals using the following symbols:
|Symbol|Value|
|------|-----|
|`I`|1|
|`V`|5|
|`X`|10|
|`L`|50|
|`C`|100|
|`D`|500|
|`M`|1000|
Each of the symbols written by themself is equal to the value of the symbol. Different symbols written from largest to smallest are equal to the sum of the symbols. Two to three of the same symbol written consecutively is equal to the sum of those symbols (only true for `I`s, `X`s, `C`s or `M`s ). A single `I` written before a `V` or `X` is equal to 1 less than the value of the second symbol. Similarly, an `X` written before a `L` or `C` is 10 less than the second symbol, and a `C` written before a `D` or `M` is 100 less than the second symbol.
Because of the restrictions of roman numerals, numbers above 3.999 are impossible to write in the base `CENTVRION` syntax. If numbers of that size are required, see the `MAGNVM` module.
The number 0 can be expressed with the keyword `NVLLVS`.
#### Negative numbers
Negative numbers can be expressed as `NVLLVS` minus the value. For an explicit definition of negative numbers, see the `SVBNVLLA` module.
### Floats
The base `CENTVRION` syntax does not allow for floats. However, the `FRACTIO` module adds a syntax for fractions.
### Booleans
Booleans are denoted with the keywords `VERITAS` for true and `FALSITAS` for false.
### Arrays
Arrays are defined using square brackets (`[]`) and commas (`,`):
![Array literal](snippets/array_literal.png)
An array of integers can also be initialized with the `VSQVE` keyword. The range is inclusive on both ends:
![Array with VSQVE](snippets/array_vsqve.png)
```
> [I, II, III, IV, V, VI, VII, VIII, IX, X]
```
Individual elements can be accessed by index using square brackets. Indexing is 1-based, so `I` refers to the first element:
![Array indexing](snippets/array_index.png)
```
> I
```
A sub-array can be extracted with `VSQVE` inside the index brackets. Both bounds are inclusive and 1-based:
![Array slicing](snippets/array_slice.png)
```
> [XX, XXX, XL]
```
### Dicts (TABVLA)
Dicts are key-value maps created with the `TABVLA` keyword and curly braces:
![Dict creation](snippets/dict_create.png)
Keys must be strings or integers. Values are accessed and assigned with square brackets:
![Dict access](snippets/dict_access.png)
Iterating over a dict with `PER` loops over its keys:
![Dict iteration](snippets/dict_per.png)
`LONGITVDO(dict)` returns the number of entries. `CLAVES(dict)` returns the keys as an array.
## Conditionals
### SI/TVNC
If-then statements are denoted with the keywords `SI` (if) and `TVNC` (then). Thus, the code
![SI/TVNC](snippets/si_tvnc.png)
Will return `I` (1), as the conditional evaluates `x` to be true.
### Boolean expressions
In conditionals, `EST` functions as an equality evaluation, `DISPAR` as not-equal, and `MINVS` (<) and `PLVS` (>) function as inequality evaluation.
### ALIVD
When using `SI`/`TVNC` statements, you can also use `ALIVD` as an "else".
![ALIVD](snippets/alivd.png)
```
> I
```
`SI` statements may follow immediately after `ALIVD`.
![ALIVD SI](snippets/alivd_si.png)
```
> II
```
### Boolean operators
The keyword `ET` can be used as a boolean "and". The keyword `AVT` can be used as a boolean "or".
![Boolean operators](snippets/boolean_ops.png)
```
> II
```
## Loops
### DONICVM loops
![DONICVM loop](snippets/donicvm.png)
```
> LV
```
### DVM loops
![DVM loop](snippets/dvm.png)
```
> XI
```
### AETERNVM loops
`AETERNVM FAC { ... }` is shorthand for an infinite loop — equivalent
to `DVM FALSITAS FAC { ... }` but without relying on `DVM`'s inverted
condition. Exit the loop with `ERVMPE` (or `REDI` from inside a function).
![AETERNVM loop](snippets/aeternvm.png)
```
> X
```
### PER loops
![PER loop](snippets/per.png)
```
> I
> II
> III
> IV
> V
```
## Error handling
Errors can be caught using `TEMPTA` (temptare = to try) and `CAPE` (capere = to catch). The `CAPE` block binds the error message to a variable as a string.
```
TEMPTA {
DESIGNA x VT I / NVLLVS
} CAPE error {
DIC(error)
}
```
```
> Division by zero
```
If the try block succeeds, the catch block is skipped. If an error occurs in the catch block, it propagates up. `TEMPTA`/`CAPE` blocks can be nested.
## Functions
Functions are defined with the `DEFINI` and `VT` keywords. The `REDI` keyword is used to return. `REDI` can also be used to end the program, if used outside of a function.
Calling a function is done with the `INVOCA` keyword.
![Function definition](snippets/function.png)
```
> CXXI
```
## First-class functions
Functions are first-class values in CENTVRION. They can be assigned to variables, passed as arguments, returned from functions, and stored in arrays or dicts.
Anonymous functions are created with the `FVNCTIO` keyword:
![FVNCTIO](snippets/fvnctio.png)
```
> XIV
```
`INVOCA` accepts any expression as the callee, not just a name:
![INVOCA expressions](snippets/invoca_expr.png)
```
> VI
> VI
> XVI
```
Note: CENTVRION does **not** have closures. When a function is called, it receives a copy of the *caller's* scope, not the scope where it was defined. Variables from a function's definition site are only available if they also exist in the caller's scope at call time.
## Built-ins
### DIC
`DIC(value, ...)`
Prints one or more values to stdout, space-separated, with integers rendered as Roman numerals. Returns the printed string.
![DIC](snippets/dic.png)
### AVDI
`AVDI()`
Reads one line from stdin and returns it as a string.
### AVDI_NVMERVS
`AVDI_NVMERVS()`
Reads one line from stdin, parses it as a Roman numeral, and returns it as an integer. Raises an error if the input is not a valid numeral.
### CONTINVA
`CONTINVA`
Skips the rest of the current loop body and continues to the next iteration (`DVM` or `PER`). Has no meaningful return value.
### ERVMPE
`ERVMPE`
Breaks out of the current loop (`DVM` or `PER`). Has no meaningful return value.
### LONGITVDO
`LONGITVDO(array)`, `LONGITVDO(string)`, or `LONGITVDO(dict)`
Returns the length of `array` (element count), `string` (character count), or `dict` (entry count) as an integer.
### CLAVES
`CLAVES(dict)`
Returns the keys of `dict` as an array.
### ORDINA
`ORDINA(array)`
Sorts an array in ascending order. Returns a new sorted array. All elements must be the same type — integers, fractions, or strings. Integers and fractions sort numerically; strings sort lexicographically.
### SENATVS
`SENATVS(bool, ...)` or `SENATVS([bool])`
Returns VERITAS if a strict majority of the arguments are VERITAS, FALSITAS otherwise. Also accepts a single array of booleans. All values must be booleans. Ties return FALSITAS.
### TYPVS
`TYPVS(value)`
Returns the type of `value` as a string: `NVMERVS` (integer), `LITTERA` (string), `VERAX` (boolean), `CATALOGVS` (list), `FRACTIO` (fraction), `TABVLA` (dict), `FVNCTIO` (function), or `NVLLVS` (null).
### DORMI
`DORMI(n)`
Sleeps for `n` seconds, where `n` is an integer, fraction, or NVLLVS (treated as 0). Returns nothing meaningful.
### QVAERE
`QVAERE(pattern, string)`
Returns an array of all non-overlapping matches of the regex `pattern` in `string`. Both arguments must be strings. Patterns use extended regular expression syntax. Returns an empty array if there are no matches. Raises an error if the pattern is invalid.
### SVBSTITVE
`SVBSTITVE(pattern, replacement, string)`
Replaces all non-overlapping matches of the regex `pattern` in `string` with `replacement`. All three arguments must be strings. The replacement string supports backreferences (`\1`, `\2`, etc.) to captured groups. Returns the resulting string. Raises an error if the pattern is invalid.
## Modules
Modules are additions to the base `CENTVRION` syntax. They add or change certain features. Modules are included in your code by having
![Module declaration](snippets/module_decl.png)
In the beginning of your source file.
Vnlike many other programming languages with modules, the modules in `CENTVRION` are not libraries that can be "imported" from other scripts written in the language. They are features of the compiler, disabled by default.
### FORS
![CVM FORS](snippets/fors.png)
The `FORS` module allows you to add randomness to your `CENTVRION` program. It adds 4 new built-in functions: `FORTVITVS_NVMERVS(int, int)`, `FORTVITA_ELECTIO(['a])`, `DECIMATIO(['a])`, and `SEMEN(int)`.
`FORTVITVS_NVMERVS(int, int)` picks a random int in the (inclusive) range of the two given ints.
`FORTVITA_ELECTIO(['a])` picks a random element from the given array. `FORTVITA_ELECTIO(array)` is identical to ```array[FORTVITVS_NVMERVS(I, LONGITVDO(array))]```.
`DECIMATIO(['a])` returns a copy of the given array with a random tenth of its elements removed. Arrays with fewer than 10 elements are returned unchanged.
`SEMEN(int)` seeds the random number generator for reproducibility.
### FRACTIO
![CVM FRACTIO](snippets/fractio.png)
The `FRACTIO` module adds floats, in the form of base 12 fractions.
In the `FRACTIO` module, `.` represents 1/12, `:` represents 1/6 and `S` represents 1/2. The symbols must be written from highest to lowest. So 3/4 would be written as "`S:.`".
Fractions can be written as an extension of integers. So 3.5 would be "`IIIS`".
The symbol `|` can be used to denote that the following fraction symbols are 1 "level down" in base 12. So after the first `|`, the fraction symbols denote 144ths instead of 12ths. So 7 and 100/144 would be "`VIIS:|::`", as "7 + 100/144" is also "7+8/12+4/144".
A single "set" of fraction symbols can only represent up to 11/12, as 12/12 can be written as 1.
### MAGNVM
![CVM MAGNVM](snippets/magnvm.png)
`MAGNVM` adds the ability to write integers larger than `MMMCMXCIX` (3.999) in your code, by adding the thousands operator, "`_`".
When `_` is added _after_ a numeric symbol, the symbol becomes 1.000 times larger. The operator can be added to the same symbol multiple times. So "`V_`" is 5.000, and "`V__`" is 5.000.000. The strict rules for integers still apply, so 4.999 cannot be written as "`IV_`", but must instead be written as "`MV_CMXCIX`".
All integer symbols except `I` may be given a `_`.
### SCRIPTA
![CVM SCRIPTA](snippets/scripta.png)
The `SCRIPTA` module adds file I/O to your `CENTVRION` program. It adds 3 new built-in functions: `LEGE`, `SCRIBE`, and `ADIVNGE`.
`LEGE(string)` reads the contents of the file at the given path and returns them as a string.
`SCRIBE(string, string)` writes the second argument to the file at the path given by the first argument, overwriting any existing content.
`ADIVNGE(string, string)` appends the second argument to the file at the path given by the first argument.
### RETE
![CVM RETE](snippets/rete.png)
The `RETE` module adds networking to your `CENTVRION` program.
`PETE(string)` performs an HTTP GET request to the given URL and returns the response body as a string.
`PETITVR(string, function)` registers a GET handler for the given path. The handler function receives a single argument: a dictionary with keys `"via"` (the request path), `"quaestio"` (query string), and `"methodus"` (HTTP method). The handler's return value becomes the response body (200 OK). Unmatched paths return a 404.
`AVSCVLTA(integer)` starts an HTTP server on the given port. This call blocks indefinitely, serving registered routes. Routes must be registered with `PETITVR` before calling `AVSCVLTA`. Ports above 3999 require the `MAGNVM` module.
### SVBNVLLA
![CVM SVBNVLLA](snippets/svbnvlla.png)
The `SVBNVLLA` module adds the ability to write negative numbers as `-II` instead of `NVLLVS-II`.
- [`DOCS.md`](DOCS.md) — full language reference: syntax, data types, built-ins, modules.
- [`language/main.pdf`](language/main.pdf) — formal grammar.
- [`examples/`](examples/) — example programs.

12
cent
View File

@@ -42,6 +42,8 @@ def main():
pos = e.source_pos
char = program_text[pos.idx] if pos.idx < len(program_text) else "?"
sys.exit(f"CENTVRION error: Invalid character {char!r} at line {pos.lineno}, column {pos.colno}")
except SyntaxError as e:
sys.exit(f"CENTVRION error: {e}")
if isinstance(program, Program):
if args["-i"]:
@@ -51,17 +53,19 @@ def main():
sys.exit(f"CENTVRION error: {e}")
else:
c_source = compile_program(program)
runtime_c = os.path.join(
runtime_dir = os.path.join(
os.path.dirname(__file__),
"centvrion", "compiler", "runtime", "cent_runtime.c"
"centvrion", "compiler", "runtime"
)
runtime_c = os.path.join(runtime_dir, "cent_runtime.c")
iason_c = os.path.join(runtime_dir, "cent_iason.c")
out_path = os.path.splitext(file_path)[0]
if args["--keep-c"]:
tmp_path = out_path + ".c"
with open(tmp_path, "w") as f:
f.write(c_source)
subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"],
["gcc", "-O2", tmp_path, runtime_c, iason_c, "-o", out_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True,
)
else:
@@ -70,7 +74,7 @@ def main():
tmp_path = tmp.name
try:
subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"],
["gcc", "-O2", tmp_path, runtime_c, iason_c, "-o", out_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True,
)
finally:

View File

@@ -1,4 +1,7 @@
import functools
import http.server
import json
import math
import re
import time
import urllib.parse
@@ -59,11 +62,13 @@ def rep_join(l):
OP_STR = {
"SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-",
"SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/",
"SYMBOL_AMPERSAND": "&",
"SYMBOL_AMPERSAND": "&", "SYMBOL_AT": "@",
"KEYWORD_RELIQVVM": "RELIQVVM",
"KEYWORD_EST": "EST", "KEYWORD_DISPAR": "DISPAR",
"KEYWORD_MINVS": "MINVS",
"KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT",
"KEYWORD_HAVD_PLVS": "HAVD_PLVS",
"KEYWORD_HAVD_MINVS": "HAVD_MINVS",
}
def single_num_to_int(i, m):
@@ -133,8 +138,11 @@ def int_to_num(n, m, s=False) -> str:
for i in thousands_chars
])
return thousands + int_to_num(n % 1000, m, s)
remainder = n % 1000
return thousands + (int_to_num(remainder, m, s) if remainder else "")
else:
if n == 0:
return "NVLLVS"
nums = []
while n > 0:
for num, i in list(NUMERALS.items())[::-1]:
@@ -172,6 +180,89 @@ def make_string(val, magnvm=False, svbnvlla=False) -> str:
else:
raise CentvrionError(f"Cannot display {val!r}")
def _roman_backref(m):
try:
n = num_to_int(m.group(1), False)
except CentvrionError:
return m.group(0)
return f"\\{n}"
def _check_arabic_backref(s):
for i in range(len(s) - 1):
if s[i] == '\\' and s[i+1].isdigit():
raise CentvrionError(f"Invalid escape sequence '\\{s[i+1]}' — use Roman numerals for backreferences")
def _romanize_replacement(s):
_check_arabic_backref(s)
return re.sub(r'\\([IVXLCDM]+)', _roman_backref, s)
def _convert_quantifier(inner):
parts = inner.split(',')
converted = []
for p in parts:
p = p.strip()
if p == '':
converted.append('')
else:
try:
converted.append(str(num_to_int(p, False)))
except CentvrionError:
return None
return '{' + ','.join(converted) + '}'
def _romanize_pattern(s):
result = []
i = 0
while i < len(s):
if s[i] == '\\' and i + 1 < len(s) and s[i+1] in 'IVXLCDM':
# backref: collect Roman numeral chars and convert
j = i + 1
while j < len(s) and s[j] in 'IVXLCDM':
j += 1
try:
n = num_to_int(s[i+1:j], False)
result.append(f'\\{n}')
except CentvrionError:
result.append(s[i:j])
i = j
elif s[i] == '\\' and i + 1 < len(s) and s[i+1].isdigit():
raise CentvrionError(f"Invalid escape sequence '\\{s[i+1]}' — use Roman numerals for backreferences")
elif s[i] == '\\' and i + 1 < len(s):
result.append(s[i:i+2])
i += 2
elif s[i] == '[':
# skip character class
j = i + 1
if j < len(s) and s[j] == '^':
j += 1
if j < len(s) and s[j] == ']':
j += 1
while j < len(s) and s[j] != ']':
if s[j] == '\\' and j + 1 < len(s):
j += 1
j += 1
result.append(s[i:j+1])
i = j + 1
elif s[i] == '{':
j = s.find('}', i)
if j == -1:
result.append(s[i])
i += 1
else:
inner = s[i+1:j]
if re.match(r'^[\d,\s]+$', inner) and re.search(r'\d', inner):
raise CentvrionError(f"Invalid quantifier '{{{inner}}}' — use Roman numerals")
converted = _convert_quantifier(inner)
if converted is not None:
result.append(converted)
else:
result.append(s[i:j+1])
i = j + 1
else:
result.append(s[i])
i += 1
return ''.join(result)
FRAC_SYMBOLS = [("S", 6), (":", 2), (".", 1)]
def frac_to_fraction(s, magnvm=False, svbnvlla=False):
@@ -201,6 +292,51 @@ def frac_to_fraction(s, magnvm=False, svbnvlla=False):
return total
def _json_to_val(obj):
if obj is None:
return ValNul()
if isinstance(obj, bool):
return ValBool(obj)
if isinstance(obj, int):
return ValInt(obj)
if isinstance(obj, Fraction):
if obj.denominator == 1:
return ValInt(obj.numerator)
return ValFrac(obj)
if isinstance(obj, str):
return ValStr(obj)
if isinstance(obj, list):
return ValList([_json_to_val(x) for x in obj])
if isinstance(obj, dict):
return ValDict({k: _json_to_val(v) for k, v in obj.items()})
raise CentvrionError(f"IASON_LEGE: unsupported JSON value of type {type(obj).__name__}")
def _val_to_json(val):
if isinstance(val, ValNul):
return None
if isinstance(val, ValBool):
return val.value()
if isinstance(val, ValInt):
return val.value()
if isinstance(val, ValFrac):
return float(val.value())
if isinstance(val, ValStr):
return val.value()
if isinstance(val, ValList):
return [_val_to_json(x) for x in val.value()]
if isinstance(val, ValDict):
out = {}
for k, v in val.value().items():
if not isinstance(k, str):
raise CentvrionError("IASON_SCRIBE: dict keys must be strings to serialize as JSON")
out[k] = _val_to_json(v)
return out
if isinstance(val, ValFunc):
raise CentvrionError("IASON_SCRIBE: cannot serialize a function")
raise CentvrionError(f"IASON_SCRIBE: cannot serialize value of type {type(val).__name__}")
def fraction_to_frac(f, magnvm=False, svbnvlla=False) -> str:
if f < 0:
if not svbnvlla:
@@ -229,8 +365,15 @@ def fraction_to_frac(f, magnvm=False, svbnvlla=False) -> str:
class Node(BaseBox):
pos = None # (lineno, colno) — set in parser productions
def eval(self, vtable):
return self._eval(vtable.copy())
try:
return self._eval(vtable.copy())
except CentvrionError as e:
if e.lineno is None and self.pos is not None:
e.lineno, e.colno = self.pos
raise
def _eval(self, vtable):
raise NotImplementedError
@@ -281,19 +424,28 @@ class DataArray(Node):
class DataRangeArray(Node):
def __init__(self, from_value, to_value) -> None:
def __init__(self, from_value, to_value, step=None) -> None:
self.from_value = from_value
self.to_value = to_value
self.step = step
def __eq__(self, other):
return type(self) == type(other) and self.from_value == other.from_value and self.to_value == other.to_value
return (type(self) == type(other)
and self.from_value == other.from_value
and self.to_value == other.to_value
and self.step == other.step)
def __repr__(self) -> str:
content_string = rep_join([self.from_value, self.to_value])
return f"RangeArray([{content_string}])"
parts = [self.from_value, self.to_value]
if self.step is not None:
parts.append(self.step)
return f"RangeArray([{rep_join(parts)}])"
def print(self):
return f"[{self.from_value.print()} VSQVE {self.to_value.print()}]"
base = f"[{self.from_value.print()} VSQVE {self.to_value.print()}"
if self.step is not None:
base += f" GRADV {self.step.print()}"
return base + "]"
def _eval(self, vtable):
vtable, from_val = self.from_value.eval(vtable)
@@ -302,7 +454,25 @@ class DataRangeArray(Node):
raise CentvrionError("Range bounds must be numbers")
from_int = from_val.value() or 0
to_int = to_val.value() or 0
return vtable, ValList([ValInt(i) for i in range(from_int, to_int + 1)])
if self.step is None:
return vtable, ValList([ValInt(i) for i in range(from_int, to_int + 1)])
vtable, step_val = self.step.eval(vtable)
if not isinstance(step_val, ValInt):
raise CentvrionError("Range step must be a number")
step_int = step_val.value()
if step_int == 0:
raise CentvrionError("Range step cannot be zero")
items = []
i = from_int
if step_int > 0:
while i <= to_int:
items.append(ValInt(i))
i += step_int
else:
while i >= to_int:
items.append(ValInt(i))
i += step_int
return vtable, ValList(items)
class DataDict(Node):
@@ -512,42 +682,97 @@ class Designa(Node):
return vtable, ValNul()
def _index_get(container, index):
if isinstance(container, ValDict):
if not isinstance(index, (ValStr, ValInt)):
raise CentvrionError("Dict key must be a string or integer")
d = container.value()
k = index.value()
if k not in d:
raise CentvrionError("Key not found in dict")
return d[k]
if isinstance(container, ValList):
i = index.value()
lst = container.value()
if i < 1 or i > len(lst):
raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}")
return lst[i - 1]
if isinstance(container, ValStr):
if isinstance(index, ValInt):
i = index.value()
elif isinstance(index, ValFrac) and index.value().denominator == 1:
i = index.value().numerator
else:
raise CentvrionError("String index must be a number")
s = container.value()
if i < 1 or i > len(s):
raise CentvrionError(f"Index {i} out of range for string of length {len(s)}")
return ValStr(s[i - 1])
raise CentvrionError("Cannot index into a non-array, non-dict value")
def _index_set(container, index, value):
if isinstance(container, ValDict):
if not isinstance(index, (ValStr, ValInt)):
raise CentvrionError("Dict key must be a string or integer")
d = dict(container.value())
d[index.value()] = value
return ValDict(d)
if isinstance(container, ValList):
i = index.value()
lst = list(container.value())
if i < 1 or i > len(lst):
raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}")
lst[i - 1] = value
return ValList(lst)
if isinstance(container, ValStr):
if isinstance(index, ValInt):
i = index.value()
elif isinstance(index, ValFrac) and index.value().denominator == 1:
i = index.value().numerator
else:
raise CentvrionError("String index must be a number")
if not isinstance(value, ValStr) or len(value.value()) != 1:
raise CentvrionError("String index assignment requires a single character")
s = container.value()
if i < 1 or i > len(s):
raise CentvrionError(f"Index {i} out of range for string of length {len(s)}")
return ValStr(s[:i - 1] + value.value() + s[i:])
raise CentvrionError("Cannot assign to index of a non-array, non-dict value")
class DesignaIndex(Node):
def __init__(self, variable: ID, index, value) -> None:
def __init__(self, variable: ID, indices, value) -> None:
self.id = variable
self.index = index
self.indices = indices if isinstance(indices, list) else [indices]
self.value = value
def __eq__(self, other):
return type(self) == type(other) and self.id == other.id and self.index == other.index and self.value == other.value
return type(self) == type(other) and self.id == other.id and self.indices == other.indices and self.value == other.value
def __repr__(self) -> str:
return f"DesignaIndex({self.id!r}, {self.index!r}, {self.value!r})"
return f"DesignaIndex({self.id!r}, {self.indices!r}, {self.value!r})"
def print(self):
return f"DESIGNA {self.id.print()}[{self.index.print()}] VT {self.value.print()}"
idx_str = ''.join(f'[{idx.print()}]' for idx in self.indices)
return f"DESIGNA {self.id.print()}{idx_str} VT {self.value.print()}"
def _eval(self, vtable):
vtable, index = self.index.eval(vtable)
evaluated_indices = []
for idx_expr in self.indices:
vtable, idx_val = idx_expr.eval(vtable)
evaluated_indices.append(idx_val)
vtable, val = self.value.eval(vtable)
if self.id.name not in vtable:
raise CentvrionError(f"Undefined variable: {self.id.name}")
target = vtable[self.id.name]
if isinstance(target, ValDict):
if not isinstance(index, (ValStr, ValInt)):
raise CentvrionError("Dict key must be a string or integer")
d = dict(target.value())
d[index.value()] = val
vtable[self.id.name] = ValDict(d)
return vtable, ValNul()
if not isinstance(target, ValList):
raise CentvrionError(f"{self.id.name} is not an array or dict")
i = index.value()
lst = list(target.value())
if i < 1 or i > len(lst):
raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}")
lst[i - 1] = val
vtable[self.id.name] = ValList(lst)
root = vtable[self.id.name]
containers = [root]
for idx in evaluated_indices[:-1]:
containers.append(_index_get(containers[-1], idx))
new_val = _index_set(containers[-1], evaluated_indices[-1], val)
for i in range(len(containers) - 2, -1, -1):
new_val = _index_set(containers[i], evaluated_indices[i], new_val)
vtable[self.id.name] = new_val
return vtable, ValNul()
@@ -720,6 +945,20 @@ class BinOp(Node):
return f"({self.left.print()} {OP_STR[self.op]} {self.right.print()})"
def _eval(self, vtable):
# Short-circuit for logical operators
if self.op == "KEYWORD_AVT":
vtable, left = self.left.eval(vtable)
if bool(left):
return vtable, ValBool(True)
vtable, right = self.right.eval(vtable)
return vtable, ValBool(bool(right))
if self.op == "KEYWORD_ET":
vtable, left = self.left.eval(vtable)
if not bool(left):
return vtable, ValBool(False)
vtable, right = self.right.eval(vtable)
return vtable, ValBool(bool(right))
vtable, left = self.left.eval(vtable)
vtable, right = self.right.eval(vtable)
lv, rv = left.value(), right.value()
@@ -733,6 +972,10 @@ class BinOp(Node):
return vtable, ValNul()
result = (lv or 0) + (rv or 0)
return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
case "SYMBOL_AT":
if not isinstance(left, ValList) or not isinstance(right, ValList):
raise CentvrionError("@ requires two arrays")
return vtable, ValList(list(lv) + list(rv))
case "SYMBOL_AMPERSAND":
magnvm = "MAGNVM" in vtable["#modules"]
svbnvlla = "SVBNVLLA" in vtable["#modules"]
@@ -778,14 +1021,24 @@ class BinOp(Node):
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot compare strings or arrays with PLVS")
return vtable, ValBool((lv or 0) > (rv or 0))
case "KEYWORD_HAVD_PLVS":
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot compare strings or arrays with HAVD_PLVS")
return vtable, ValBool((lv or 0) <= (rv or 0))
case "KEYWORD_HAVD_MINVS":
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot compare strings or arrays with HAVD_MINVS")
return vtable, ValBool((lv or 0) >= (rv or 0))
case "KEYWORD_EST":
if ((isinstance(left, ValInt) and lv == 0 and isinstance(right, ValNul)) or
(isinstance(left, ValNul) and isinstance(right, ValInt) and rv == 0)):
return vtable, ValBool(True)
return vtable, ValBool(lv == rv)
case "KEYWORD_DISPAR":
if ((isinstance(left, ValInt) and lv == 0 and isinstance(right, ValNul)) or
(isinstance(left, ValNul) and isinstance(right, ValInt) and rv == 0)):
return vtable, ValBool(False)
return vtable, ValBool(lv != rv)
case "KEYWORD_ET":
return vtable, ValBool(bool(left) and bool(right))
case "KEYWORD_AVT":
return vtable, ValBool(bool(left) or bool(right))
case _:
raise Exception(self.op)
@@ -1034,6 +1287,10 @@ class PerStatement(Node):
def __eq__(self, other):
return type(self) == type(other) and self.data_list == other.data_list and self.variable_name == other.variable_name and self.statements == other.statements
@property
def destructure(self):
return isinstance(self.variable_name, list)
def __repr__(self) -> str:
test = repr(self.data_list)
variable_name = repr(self.variable_name)
@@ -1043,7 +1300,23 @@ class PerStatement(Node):
def print(self):
body = "\n".join(s.print() for s in self.statements)
return f"PER {self.variable_name.print()} IN {self.data_list.print()} FAC {{\n{body}\n}}"
if self.destructure:
var_str = ", ".join(v.print() for v in self.variable_name)
else:
var_str = self.variable_name.print()
return f"PER {var_str} IN {self.data_list.print()} FAC {{\n{body}\n}}"
def _assign_loop_var(self, vtable, item):
if self.destructure:
if not isinstance(item, ValList):
raise CentvrionError("Cannot destructure non-array value in PER loop")
if len(item.value()) != len(self.variable_name):
raise CentvrionError(
f"Destructuring mismatch: {len(self.variable_name)} targets, {len(item.value())} values")
for id_node, val in zip(self.variable_name, item.value()):
vtable[id_node.name] = val
else:
vtable[self.variable_name.name] = item
def _eval(self, vtable):
vtable, array = self.data_list.eval(vtable)
@@ -1052,10 +1325,9 @@ class PerStatement(Node):
array = ValList(keys)
if not isinstance(array, ValList):
raise CentvrionError("PER requires an array or dict")
variable_name = self.variable_name.name
last_val = ValNul()
for item in array:
vtable[variable_name] = item
self._assign_loop_var(vtable, item)
for statement in self.statements:
vtable, val = statement.eval(vtable)
if vtable["#break"] or vtable["#continue"] or vtable["#return"] is not None:
@@ -1103,7 +1375,7 @@ class TemptaStatement(Node):
if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]:
return vtable, last_val
except CentvrionError as e:
vtable[self.error_var.name] = ValStr(str(e))
vtable[self.error_var.name] = ValStr(e.msg)
for statement in self.catch_statements:
vtable, last_val = statement.eval(vtable)
if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]:
@@ -1111,6 +1383,22 @@ class TemptaStatement(Node):
return vtable, last_val
def _call_func(func: ValFunc, args: list, vtable: dict, callee_desc: str = "function"):
if len(args) != len(func.params):
raise CentvrionError(
f"{callee_desc} expects {len(func.params)} argument(s), got {len(args)}"
)
func_vtable = vtable.copy()
for i, param in enumerate(func.params):
func_vtable[param.name] = args[i]
func_vtable["#return"] = None
for statement in func.body:
func_vtable, _ = statement.eval(func_vtable)
if func_vtable["#return"] is not None:
return func_vtable["#return"]
return ValNul()
class Invoca(Node):
def __init__(self, callee, parameters) -> None:
self.callee = callee
@@ -1137,21 +1425,9 @@ class Invoca(Node):
callee_desc = (self.callee.name
if isinstance(self.callee, ID) else "expression")
raise CentvrionError(f"{callee_desc} is not a function")
if len(params) != len(func.params):
callee_desc = (self.callee.name
if isinstance(self.callee, ID) else "FVNCTIO")
raise CentvrionError(
f"{callee_desc} expects {len(func.params)} argument(s), got {len(params)}"
)
func_vtable = vtable.copy()
for i, param in enumerate(func.params):
func_vtable[param.name] = params[i]
func_vtable["#return"] = None
for statement in func.body:
func_vtable, _ = statement.eval(func_vtable)
if func_vtable["#return"] is not None:
return vtable, func_vtable["#return"]
return vtable, ValNul()
callee_desc = (self.callee.name
if isinstance(self.callee, ID) else "FVNCTIO")
return vtable, _call_func(func, params, vtable, callee_desc)
class BuiltIn(Node):
@@ -1185,6 +1461,13 @@ class BuiltIn(Node):
raise CentvrionError(f"Invalid numeral input: {raw!r}")
case "AVDI":
return vtable, ValStr(input())
case "NVMERVS":
if len(params) != 1:
raise CentvrionError("NVMERVS takes exactly I argument")
val = params[0]
if not isinstance(val, ValStr):
raise CentvrionError(f"NVMERVS expects a string, got {type(val).__name__}")
return vtable, ValInt(num_to_int(val.value(), magnvm, svbnvlla))
case "DIC":
print_string = ' '.join(
make_string(i, magnvm, svbnvlla) for i in params
@@ -1244,6 +1527,26 @@ class BuiltIn(Node):
if isinstance(params[0], (ValList, ValStr, ValDict)):
return vtable, ValInt(len(params[0].value()))
raise CentvrionError("LONGITVDO requires an array, string, or dict")
case "LITTERA":
if len(params) != 1:
raise CentvrionError("LITTERA takes exactly I argument")
return vtable, ValStr(make_string(params[0], magnvm, svbnvlla))
case "MAIVSCVLA":
if len(params) != 1:
raise CentvrionError("MAIVSCVLA takes exactly I argument")
val = params[0]
if not isinstance(val, ValStr):
raise CentvrionError(f"MAIVSCVLA expects a string, got {type(val).__name__}")
s = val.value()
return vtable, ValStr("".join(chr(ord(c) - 32) if "a" <= c <= "z" else c for c in s))
case "MINVSCVLA":
if len(params) != 1:
raise CentvrionError("MINVSCVLA takes exactly I argument")
val = params[0]
if not isinstance(val, ValStr):
raise CentvrionError(f"MINVSCVLA expects a string, got {type(val).__name__}")
s = val.value()
return vtable, ValStr("".join(chr(ord(c) + 32) if "A" <= c <= "Z" else c for c in s))
case "CLAVES":
if not isinstance(params[0], ValDict):
raise CentvrionError("CLAVES requires a dict")
@@ -1252,17 +1555,130 @@ class BuiltIn(Node):
case "EVERRE":
print("\033[2J\033[H", end="", flush=True)
return vtable, ValNul()
case "ADDE":
if len(params) != 2:
raise CentvrionError("ADDE takes exactly II arguments")
if not isinstance(params[0], ValList):
raise CentvrionError("ADDE requires an array")
return vtable, ValList(list(params[0].value()) + [params[1]])
case "TOLLE":
if len(params) != 2:
raise CentvrionError("TOLLE takes exactly II arguments")
if not isinstance(params[0], ValList):
raise CentvrionError("TOLLE requires an array")
items = list(params[0].value())
idx = _to_index_int(params[1])
if idx < 1 or idx > len(items):
raise CentvrionError(f"Index {idx} out of range for array of length {len(items)}")
return vtable, ValList(items[:idx - 1] + items[idx:])
case "INSERE":
if len(params) != 3:
raise CentvrionError("INSERE takes exactly III arguments")
if not isinstance(params[0], ValList):
raise CentvrionError("INSERE requires an array")
items = list(params[0].value())
idx = _to_index_int(params[1])
if idx < 1 or idx > len(items) + 1:
raise CentvrionError(f"Index {idx} out of range for array of length {len(items)}")
return vtable, ValList(items[:idx - 1] + [params[2]] + items[idx - 1:])
case "NECTE":
if len(params) != 2:
raise CentvrionError("NECTE takes exactly II arguments")
if not isinstance(params[0], ValList) or not isinstance(params[1], ValList):
raise CentvrionError("NECTE requires two arrays")
a, b = list(params[0].value()), list(params[1].value())
if len(a) != len(b):
raise CentvrionError("NECTE requires arrays of equal length")
return vtable, ValList([ValList([x, y]) for x, y in zip(a, b)])
case "IVNGE":
if len(params) != 2:
raise CentvrionError("IVNGE takes exactly II arguments")
if not isinstance(params[0], ValList) or not isinstance(params[1], ValList):
raise CentvrionError("IVNGE requires two arrays")
keys, vals = list(params[0].value()), list(params[1].value())
if len(keys) != len(vals):
raise CentvrionError("IVNGE requires arrays of equal length")
d = {}
for k, v in zip(keys, vals):
if not isinstance(k, (ValStr, ValInt)):
raise CentvrionError("Dict keys must be strings or integers")
d[k.value()] = v
return vtable, ValDict(d)
case "ORDINA":
if not 1 <= len(params) <= 2:
raise CentvrionError("ORDINA takes 1 or 2 arguments")
if not isinstance(params[0], ValList):
raise CentvrionError("ORDINA requires an array")
items = list(params[0].value())
if not items:
return vtable, ValList([])
if len(params) == 2:
cmp = params[1]
if not isinstance(cmp, ValFunc):
raise CentvrionError("ORDINA comparator must be a function")
if len(cmp.params) != 2:
raise CentvrionError("ORDINA comparator must take 2 arguments")
def adapter(a, b):
r = _call_func(cmp, [a, b], vtable, "ORDINA comparator")
if not isinstance(r, ValBool):
raise CentvrionError("ORDINA comparator must return VERAX")
if r.value():
return -1
r2 = _call_func(cmp, [b, a], vtable, "ORDINA comparator")
if not isinstance(r2, ValBool):
raise CentvrionError("ORDINA comparator must return VERAX")
return 1 if r2.value() else 0
return vtable, ValList(sorted(items, key=functools.cmp_to_key(adapter)))
all_numeric = all(isinstance(i, (ValInt, ValFrac)) for i in items)
all_string = all(isinstance(i, ValStr) for i in items)
if not (all_numeric or all_string):
raise CentvrionError("ORDINA requires all elements to be numbers or all strings")
return vtable, ValList(sorted(items, key=lambda v: v.value()))
case "MVTA":
if len(params) != 2:
raise CentvrionError("MVTA takes II arguments")
if not isinstance(params[0], ValList):
raise CentvrionError("MVTA requires an array")
fn = params[1]
if not isinstance(fn, ValFunc):
raise CentvrionError("MVTA requires a function")
if len(fn.params) != 1:
raise CentvrionError("MVTA function must take I argument")
out = [_call_func(fn, [item], vtable, "MVTA function")
for item in params[0].value()]
return vtable, ValList(out)
case "CRIBRA":
if len(params) != 2:
raise CentvrionError("CRIBRA takes II arguments")
if not isinstance(params[0], ValList):
raise CentvrionError("CRIBRA requires an array")
fn = params[1]
if not isinstance(fn, ValFunc):
raise CentvrionError("CRIBRA requires a function")
if len(fn.params) != 1:
raise CentvrionError("CRIBRA predicate must take I argument")
out = []
for item in params[0].value():
r = _call_func(fn, [item], vtable, "CRIBRA predicate")
if not isinstance(r, ValBool):
raise CentvrionError("CRIBRA predicate must return VERAX")
if r.value():
out.append(item)
return vtable, ValList(out)
case "CONFLA":
if len(params) != 3:
raise CentvrionError("CONFLA takes III arguments")
if not isinstance(params[0], ValList):
raise CentvrionError("CONFLA requires an array")
fn = params[2]
if not isinstance(fn, ValFunc):
raise CentvrionError("CONFLA requires a function")
if len(fn.params) != 2:
raise CentvrionError("CONFLA function must take II arguments")
acc = params[1]
for item in params[0].value():
acc = _call_func(fn, [acc, item], vtable, "CONFLA function")
return vtable, acc
case "TYPVS":
type_map = {
ValInt: "NVMERVS", ValStr: "LITTERA", ValBool: "VERAX",
@@ -1312,7 +1728,7 @@ class BuiltIn(Node):
try:
matches = [
ValStr(m.group(0))
for m in re.finditer(pattern.value(), text.value())
for m in re.finditer(_romanize_pattern(pattern.value()), text.value())
]
except re.error as e:
raise CentvrionError(f"Invalid regex: {e}")
@@ -1324,10 +1740,26 @@ class BuiltIn(Node):
if not isinstance(pattern, ValStr) or not isinstance(replacement, ValStr) or not isinstance(text, ValStr):
raise CentvrionError("SVBSTITVE requires three strings")
try:
result = re.sub(pattern.value(), replacement.value(), text.value())
result = re.sub(
_romanize_pattern(pattern.value()),
_romanize_replacement(replacement.value()),
text.value()
)
except re.error as e:
raise CentvrionError(f"Invalid regex: {e}")
return vtable, ValStr(result)
case "SCINDE":
string = params[0]
delimiter = params[1]
if not isinstance(string, ValStr) or not isinstance(delimiter, ValStr):
raise CentvrionError("SCINDE requires two strings")
s = string.value()
d = delimiter.value()
if d == "":
parts = [ValStr(c) for c in s]
else:
parts = [ValStr(p) for p in s.split(d)]
return vtable, ValList(parts)
case "PETE":
if "RETE" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'PETE' without module 'RETE'")
@@ -1397,6 +1829,30 @@ class BuiltIn(Node):
server = http.server.HTTPServer(("0.0.0.0", port.value()), _CentHandler)
server.serve_forever()
return vtable, ValNul()
case "IASON_LEGE":
if "IASON" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'IASON_LEGE' without module 'IASON'")
if len(params) != 1:
raise CentvrionError("IASON_LEGE takes exactly I argument")
s = params[0]
if not isinstance(s, ValStr):
raise CentvrionError("IASON_LEGE requires a string")
fractio = "FRACTIO" in vtable["#modules"]
try:
if fractio:
parsed = json.loads(s.value(), parse_float=Fraction)
else:
parsed = json.loads(s.value(), parse_float=lambda x: math.floor(float(x)))
except json.JSONDecodeError as e:
raise CentvrionError(f"IASON_LEGE: invalid JSON: {e.msg}")
return vtable, _json_to_val(parsed)
case "IASON_SCRIBE":
if "IASON" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'IASON_SCRIBE' without module 'IASON'")
if len(params) != 1:
raise CentvrionError("IASON_SCRIBE takes exactly I argument")
obj = _val_to_json(params[0])
return vtable, ValStr(json.dumps(obj, ensure_ascii=False))
case _:
raise NotImplementedError(self.builtin)

View File

@@ -7,17 +7,28 @@ from centvrion.ast_nodes import (
num_to_int, frac_to_fraction,
)
def _err(node, msg):
"""Build a CentvrionError stamped with a node's source position, if any."""
pos = getattr(node, "pos", None)
if pos is not None:
return CentvrionError(msg, pos[0], pos[1])
return CentvrionError(msg)
_BINOP_FN = {
"SYMBOL_PLUS": "cent_add",
"SYMBOL_MINUS": "cent_sub",
"SYMBOL_TIMES": "cent_mul",
"SYMBOL_DIVIDE": "cent_div",
"SYMBOL_AMPERSAND": "cent_concat",
"SYMBOL_AT": "cent_array_concat",
"KEYWORD_RELIQVVM": "cent_mod",
"KEYWORD_EST": "cent_eq",
"KEYWORD_DISPAR": "cent_neq",
"KEYWORD_MINVS": "cent_lt",
"KEYWORD_PLVS": "cent_gt",
"KEYWORD_HAVD_PLVS": "cent_lte",
"KEYWORD_HAVD_MINVS": "cent_gte",
"KEYWORD_ET": "cent_and",
"KEYWORD_AVT": "cent_or",
}
@@ -81,7 +92,7 @@ def emit_expr(node, ctx):
if isinstance(node, Fractio):
if not ctx.has_module("FRACTIO"):
raise CentvrionError("Cannot use fraction literals without 'FRACTIO' module")
raise _err(node, "Cannot use fraction literals without 'FRACTIO' module")
tmp = ctx.fresh_tmp()
magnvm = "MAGNVM" in ctx.modules
svbnvlla = "SVBNVLLA" in ctx.modules
@@ -93,6 +104,25 @@ def emit_expr(node, ctx):
return [f'CentValue {tmp} = cent_scope_get(&_scope, "{node.name}");'], tmp
if isinstance(node, BinOp):
# Short-circuit for logical operators
if node.op in ("KEYWORD_AVT", "KEYWORD_ET"):
l_lines, l_var = emit_expr(node.left, ctx)
r_lines, r_var = emit_expr(node.right, ctx)
tmp = ctx.fresh_tmp()
if node.op == "KEYWORD_AVT":
lines = l_lines + [f"CentValue {tmp};"]
lines += [f"if (cent_truthy({l_var})) {{ {tmp} = cent_bool(1); }} else {{"]
lines += [f" {l}" for l in r_lines]
lines += [f" {tmp} = cent_bool(cent_truthy({r_var}));"]
lines += ["}"]
else:
lines = l_lines + [f"CentValue {tmp};"]
lines += [f"if (!cent_truthy({l_var})) {{ {tmp} = cent_bool(0); }} else {{"]
lines += [f" {l}" for l in r_lines]
lines += [f" {tmp} = cent_bool(cent_truthy({r_var}));"]
lines += ["}"]
return lines, tmp
l_lines, l_var = emit_expr(node.left, ctx)
r_lines, r_var = emit_expr(node.right, ctx)
tmp = ctx.fresh_tmp()
@@ -107,7 +137,7 @@ def emit_expr(node, ctx):
if isinstance(node, UnaryMinus):
inner_lines, inner_var = emit_expr(node.expr, ctx)
tmp = ctx.fresh_tmp()
return inner_lines + [f"CentValue {tmp} = cent_int(-{inner_var}.ival);"], tmp
return inner_lines + [f"CentValue {tmp} = cent_neg({inner_var});"], tmp
if isinstance(node, UnaryNot):
inner_lines, inner_var = emit_expr(node.expr, ctx)
@@ -144,10 +174,21 @@ def emit_expr(node, ctx):
hi_lines, hi_var = emit_expr(node.to_value, ctx)
tmp = ctx.fresh_tmp()
i_var = ctx.fresh_tmp()
cap = f"({hi_var}.ival >= {lo_var}.ival ? (int)({hi_var}.ival - {lo_var}.ival + 1) : 0)"
lines = lo_lines + hi_lines + [
f"CentValue {tmp} = cent_list_new({cap});",
f"for (long {i_var} = {lo_var}.ival; {i_var} <= {hi_var}.ival; {i_var}++) {{",
if node.step is None:
cap = f"({hi_var}.ival >= {lo_var}.ival ? (int)({hi_var}.ival - {lo_var}.ival + 1) : 0)"
lines = lo_lines + hi_lines + [
f"CentValue {tmp} = cent_list_new({cap});",
f"for (long {i_var} = {lo_var}.ival; {i_var} <= {hi_var}.ival; {i_var}++) {{",
f" cent_list_push(&{tmp}, cent_int({i_var}));",
"}",
]
return lines, tmp
step_lines, step_var = emit_expr(node.step, ctx)
lines = lo_lines + hi_lines + step_lines + [
f'if ({step_var}.type != CENT_INT) cent_type_error("Range step must be a number");',
f'if ({step_var}.ival == 0) cent_runtime_error("Range step cannot be zero");',
f"CentValue {tmp} = cent_list_new(0);",
f"for (long {i_var} = {lo_var}.ival; ({step_var}.ival > 0 ? {i_var} <= {hi_var}.ival : {i_var} >= {hi_var}.ival); {i_var} += {step_var}.ival) {{",
f" cent_list_push(&{tmp}, cent_int({i_var}));",
"}",
]
@@ -215,6 +256,23 @@ def _emit_builtin(node, ctx):
case "LONGITVDO":
lines.append(f"CentValue {tmp} = cent_longitudo({param_vars[0]});")
case "LITTERA":
lines.append(f"CentValue {tmp} = cent_littera({param_vars[0]});")
case "MAIVSCVLA":
if len(param_vars) != 1:
lines.append(f'cent_runtime_error("MAIVSCVLA takes exactly I argument");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_maivscvla({param_vars[0]});")
case "MINVSCVLA":
if len(param_vars) != 1:
lines.append(f'cent_runtime_error("MINVSCVLA takes exactly I argument");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_minvscvla({param_vars[0]});")
case "FORTVITVS_NVMERVS":
if not ctx.has_module("FORS"):
lines.append('cent_runtime_error("FORS module required for FORTVITVS_NVMERVS");')
@@ -261,7 +319,50 @@ def _emit_builtin(node, ctx):
lines.append(f"CentValue {tmp} = cent_dict_keys({param_vars[0]});")
case "ORDINA":
lines.append(f"CentValue {tmp} = cent_ordina({param_vars[0]});")
if len(param_vars) == 1:
lines.append(f"CentValue {tmp} = cent_ordina({param_vars[0]});")
elif len(param_vars) == 2:
lines.append(
f"CentValue {tmp} = cent_ordina_cmp({param_vars[0]}, {param_vars[1]}, _scope);"
)
else:
raise _err(node, "ORDINA takes 1 or 2 arguments")
case "MVTA":
if len(param_vars) != 2:
raise _err(node, "MVTA takes II arguments")
lines.append(
f"CentValue {tmp} = cent_mvta({param_vars[0]}, {param_vars[1]}, _scope);"
)
case "CRIBRA":
if len(param_vars) != 2:
raise _err(node, "CRIBRA takes II arguments")
lines.append(
f"CentValue {tmp} = cent_cribra({param_vars[0]}, {param_vars[1]}, _scope);"
)
case "CONFLA":
if len(param_vars) != 3:
raise _err(node, "CONFLA takes III arguments")
lines.append(
f"CentValue {tmp} = cent_confla({param_vars[0]}, {param_vars[1]}, {param_vars[2]}, _scope);"
)
case "ADDE":
lines.append(f"CentValue {tmp} = cent_adde({param_vars[0]}, {param_vars[1]});")
case "TOLLE":
lines.append(f"CentValue {tmp} = cent_tolle({param_vars[0]}, {param_vars[1]});")
case "INSERE":
lines.append(f"CentValue {tmp} = cent_insere({param_vars[0]}, {param_vars[1]}, {param_vars[2]});")
case "NECTE":
lines.append(f"CentValue {tmp} = cent_necte({param_vars[0]}, {param_vars[1]});")
case "IVNGE":
lines.append(f"CentValue {tmp} = cent_ivnge({param_vars[0]}, {param_vars[1]});")
case "EVERRE":
lines.append("cent_everre();")
@@ -297,12 +398,22 @@ def _emit_builtin(node, ctx):
lines.append(f"cent_adivnge({param_vars[0]}, {param_vars[1]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "NVMERVS":
if len(param_vars) != 1:
lines.append(f'cent_runtime_error("NVMERVS takes exactly I argument");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_numerus({param_vars[0]});")
case "QVAERE":
lines.append(f"CentValue {tmp} = cent_qvaere({param_vars[0]}, {param_vars[1]});")
case "SVBSTITVE":
lines.append(f"CentValue {tmp} = cent_svbstitve({param_vars[0]}, {param_vars[1]}, {param_vars[2]});")
case "SCINDE":
lines.append(f"CentValue {tmp} = cent_scinde({param_vars[0]}, {param_vars[1]});")
case "PETE":
if not ctx.has_module("RETE"):
lines.append('cent_runtime_error("RETE module required for PETE");')
@@ -326,6 +437,25 @@ def _emit_builtin(node, ctx):
lines.append(f"cent_avscvlta({param_vars[0]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "IASON_LEGE":
if not ctx.has_module("IASON"):
lines.append('cent_runtime_error("IASON module required for IASON_LEGE");')
lines.append(f"CentValue {tmp} = cent_null();")
elif len(param_vars) != 1:
raise _err(node, "IASON_LEGE takes exactly I argument")
else:
fractio_flag = "1" if ctx.has_module("FRACTIO") else "0"
lines.append(f"CentValue {tmp} = cent_iason_lege({param_vars[0]}, {fractio_flag});")
case "IASON_SCRIBE":
if not ctx.has_module("IASON"):
lines.append('cent_runtime_error("IASON module required for IASON_SCRIBE");')
lines.append(f"CentValue {tmp} = cent_null();")
elif len(param_vars) != 1:
raise _err(node, "IASON_SCRIBE takes exactly I argument")
else:
lines.append(f"CentValue {tmp} = cent_iason_scribe({param_vars[0]});")
case _:
raise NotImplementedError(node.builtin)
@@ -353,7 +483,7 @@ def _emit_invoca(node, ctx):
lines.append(f"CentScope {call_scope_var} = cent_scope_copy(&_scope);")
param_names = ctx.functions[c_func_name]
if len(param_vars) != len(param_names):
raise CentvrionError(
raise _err(node,
f"Function '{node.callee.name}' expects {len(param_names)} argument(s), "
f"got {len(param_vars)}"
)

View File

@@ -11,21 +11,45 @@ def emit_stmt(node, ctx):
Emit C code for a CENTVRION statement node.
Returns lines — list of C statements.
"""
body = _emit_stmt_body(node, ctx)
pos = getattr(node, "pos", None)
if pos is not None:
return [f"_cent_current_line = {pos[0]};"] + body
return body
def _emit_stmt_body(node, ctx):
if isinstance(node, Designa):
val_lines, val_var = emit_expr(node.value, ctx)
return val_lines + [f'cent_scope_set(&_scope, "{node.id.name}", {val_var});']
if isinstance(node, DesignaIndex):
idx_lines, idx_var = emit_expr(node.index, ctx)
lines = []
idx_vars = []
for idx_expr in node.indices:
idx_lines, idx_var = emit_expr(idx_expr, ctx)
lines += idx_lines
idx_vars.append(idx_var)
val_lines, val_var = emit_expr(node.value, ctx)
arr_tmp = ctx.fresh_tmp()
return (
idx_lines + val_lines + [
f'CentValue {arr_tmp} = cent_scope_get(&_scope, "{node.id.name}");',
f"cent_list_index_set(&{arr_tmp}, {idx_var}, {val_var});",
f'cent_scope_set(&_scope, "{node.id.name}", {arr_tmp});',
]
)
lines += val_lines
root_tmp = ctx.fresh_tmp()
lines.append(f'CentValue {root_tmp} = cent_scope_get(&_scope, "{node.id.name}");')
if len(idx_vars) == 1:
lines.append(f"cent_list_index_set(&{root_tmp}, {idx_vars[0]}, {val_var});")
else:
# Walk down to collect intermediate containers
container_tmps = [root_tmp]
for idx_var in idx_vars[:-1]:
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = cent_list_index({container_tmps[-1]}, {idx_var});")
container_tmps.append(tmp)
# Set at deepest level
lines.append(f"cent_list_index_set(&{container_tmps[-1]}, {idx_vars[-1]}, {val_var});")
# Rebuild up the chain
for i in range(len(container_tmps) - 2, -1, -1):
lines.append(f"cent_list_index_set(&{container_tmps[i]}, {idx_vars[i]}, {container_tmps[i + 1]});")
lines.append(f'cent_scope_set(&_scope, "{node.id.name}", {root_tmp});')
return lines
if isinstance(node, DesignaDestructure):
n = len(node.ids)
@@ -65,23 +89,44 @@ def emit_stmt(node, ctx):
if isinstance(node, PerStatement):
arr_lines, arr_var = emit_expr(node.data_list, ctx)
i_var = ctx.fresh_tmp()
var_name = node.variable_name.name
body_lines = _emit_body(node.statements, ctx)
lines = arr_lines + [
f"if ({arr_var}.type == CENT_DICT) {{",
f" for (int {i_var} = 0; {i_var} < {arr_var}.dval.len; {i_var}++) {{",
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.dval.keys[{i_var}]);',
]
lines += [f" {l}" for l in body_lines]
lines += [
" }",
"} else {",
f' if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array or dict");',
f" for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{",
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.lval.items[{i_var}]);',
]
lines += [f" {l}" for l in body_lines]
lines += [" }", "}"]
if node.destructure:
# Destructuring PER — each element must be a list
elem_var = ctx.fresh_tmp()
assign_lines = [
f"CentValue {elem_var} = {arr_var}.lval.items[{i_var}];",
f'if ({elem_var}.type != CENT_LIST) cent_type_error("Cannot destructure non-array value in PER loop");',
f'if ({elem_var}.lval.len != {len(node.variable_name)}) cent_runtime_error("Destructuring mismatch");',
]
for j, id_node in enumerate(node.variable_name):
tmp = ctx.fresh_tmp()
assign_lines.append(f"CentValue {tmp} = cent_list_index({elem_var}, cent_int({j + 1}));")
assign_lines.append(f'cent_scope_set(&_scope, "{id_node.name}", {tmp});')
lines = arr_lines + [
f'if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array");',
f"for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{",
]
lines += [f" {l}" for l in assign_lines]
lines += [f" {l}" for l in body_lines]
lines += ["}"]
else:
var_name = node.variable_name.name
lines = arr_lines + [
f"if ({arr_var}.type == CENT_DICT) {{",
f" for (int {i_var} = 0; {i_var} < {arr_var}.dval.len; {i_var}++) {{",
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.dval.keys[{i_var}]);',
]
lines += [f" {l}" for l in body_lines]
lines += [
" }",
"} else {",
f' if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array or dict");',
f" for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{",
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.lval.items[{i_var}]);',
]
lines += [f" {l}" for l in body_lines]
lines += [" }", "}"]
return lines
if isinstance(node, Defini):

View File

@@ -58,6 +58,7 @@ def compile_program(program):
# Includes
lines += [
f'#include "{_RUNTIME_DIR}/cent_runtime.h"',
f'#include "{_RUNTIME_DIR}/cent_iason.h"',
"",
]
@@ -92,6 +93,8 @@ def compile_program(program):
lines.append(" cent_init();")
if "MAGNVM" in ctx.modules:
lines.append(" cent_magnvm = 1;")
if "SVBNVLLA" in ctx.modules:
lines.append(" cent_svbnvlla = 1;")
lines.append(" CentScope _scope = {0};")
lines.append(" CentValue _return_val = cent_null();")

View File

@@ -0,0 +1,426 @@
#include "cent_iason.h"
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------- shared helpers ----------------------------------------- */
static long iason_gcd(long a, long b) {
if (a < 0) a = -a;
if (b < 0) b = -b;
while (b) { long t = b; b = a % b; a = t; }
return a ? a : 1;
}
static CentValue iason_frac_reduce(long num, long den) {
if (den < 0) { num = -num; den = -den; }
long g = iason_gcd(num, den);
num /= g; den /= g;
if (den == 1) return cent_int(num);
return cent_frac(num, den);
}
/* ---------- parser ------------------------------------------------- */
typedef struct {
const char *src;
size_t pos;
size_t len;
int fractio;
} IasonParser;
static void iason_die(const char *msg) {
cent_runtime_error(msg);
}
static void iason_skip_ws(IasonParser *p) {
while (p->pos < p->len) {
char c = p->src[p->pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') p->pos++;
else break;
}
}
static int iason_peek(IasonParser *p) {
return (p->pos < p->len) ? (unsigned char)p->src[p->pos] : -1;
}
static void iason_expect(IasonParser *p, char c, const char *msg) {
if (p->pos >= p->len || p->src[p->pos] != c)
iason_die(msg);
p->pos++;
}
static void iason_expect_word(IasonParser *p, const char *word) {
size_t n = strlen(word);
if (p->len - p->pos < n || memcmp(p->src + p->pos, word, n) != 0)
iason_die("IASON_LEGE: invalid JSON literal");
p->pos += n;
}
/* Encode a Unicode codepoint as UTF-8 into buf; returns bytes written. */
static int iason_utf8_encode(unsigned cp, char *buf) {
if (cp <= 0x7F) { buf[0] = (char)cp; return 1; }
if (cp <= 0x7FF) { buf[0] = (char)(0xC0 | (cp >> 6));
buf[1] = (char)(0x80 | (cp & 0x3F)); return 2; }
if (cp <= 0xFFFF) { buf[0] = (char)(0xE0 | (cp >> 12));
buf[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
buf[2] = (char)(0x80 | (cp & 0x3F)); return 3; }
if (cp <= 0x10FFFF) { buf[0] = (char)(0xF0 | (cp >> 18));
buf[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
buf[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
buf[3] = (char)(0x80 | (cp & 0x3F)); return 4; }
iason_die("IASON_LEGE: codepoint out of range");
return 0;
}
static unsigned iason_read_hex4(IasonParser *p) {
if (p->len - p->pos < 4) iason_die("IASON_LEGE: truncated \\u escape");
unsigned v = 0;
for (int i = 0; i < 4; i++) {
char c = p->src[p->pos++];
v <<= 4;
if (c >= '0' && c <= '9') v |= c - '0';
else if (c >= 'a' && c <= 'f') v |= c - 'a' + 10;
else if (c >= 'A' && c <= 'F') v |= c - 'A' + 10;
else iason_die("IASON_LEGE: invalid hex in \\u escape");
}
return v;
}
/* Parses a JSON string literal at p->pos (positioned at the opening "),
returns an arena-allocated NUL-terminated UTF-8 string. */
static char *iason_parse_string(IasonParser *p) {
iason_expect(p, '"', "IASON_LEGE: expected string");
/* upper bound on output: same as remaining input (escapes shrink). */
size_t cap = (p->len - p->pos) + 1;
char *buf = cent_arena_alloc(cent_arena, cap);
size_t out = 0;
while (p->pos < p->len) {
unsigned char c = (unsigned char)p->src[p->pos++];
if (c == '"') { buf[out] = '\0'; return buf; }
if (c == '\\') {
if (p->pos >= p->len) iason_die("IASON_LEGE: trailing \\ in string");
char esc = p->src[p->pos++];
switch (esc) {
case '"': buf[out++] = '"'; break;
case '\\': buf[out++] = '\\'; break;
case '/': buf[out++] = '/'; break;
case 'b': buf[out++] = '\b'; break;
case 'f': buf[out++] = '\f'; break;
case 'n': buf[out++] = '\n'; break;
case 'r': buf[out++] = '\r'; break;
case 't': buf[out++] = '\t'; break;
case 'u': {
unsigned cp = iason_read_hex4(p);
if (cp >= 0xD800 && cp <= 0xDBFF) {
/* high surrogate; expect \uXXXX low surrogate */
if (p->len - p->pos < 6 || p->src[p->pos] != '\\' || p->src[p->pos + 1] != 'u')
iason_die("IASON_LEGE: missing low surrogate after high surrogate");
p->pos += 2;
unsigned lo = iason_read_hex4(p);
if (lo < 0xDC00 || lo > 0xDFFF)
iason_die("IASON_LEGE: invalid low surrogate");
cp = 0x10000 + (((cp - 0xD800) << 10) | (lo - 0xDC00));
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
iason_die("IASON_LEGE: stray low surrogate");
}
out += iason_utf8_encode(cp, buf + out);
break;
}
default: iason_die("IASON_LEGE: invalid escape sequence");
}
} else if (c < 0x20) {
iason_die("IASON_LEGE: unescaped control character in string");
} else {
buf[out++] = (char)c;
}
}
iason_die("IASON_LEGE: unterminated string");
return NULL;
}
/* Cap on fractional digits parsed exactly; beyond this we truncate to
keep `long` arithmetic safe (10^18 fits in int64). */
#define IASON_MAX_FRAC_DIGITS 18
static CentValue iason_parse_number(IasonParser *p) {
size_t start = p->pos;
int negative = 0;
if (p->src[p->pos] == '-') { negative = 1; p->pos++; }
/* Integer part. */
if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
iason_die("IASON_LEGE: invalid number");
if (p->src[p->pos] == '0') {
p->pos++;
} else {
while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos])) p->pos++;
}
int has_frac = 0, has_exp = 0;
size_t frac_start = 0, frac_end = 0;
if (p->pos < p->len && p->src[p->pos] == '.') {
has_frac = 1;
p->pos++;
frac_start = p->pos;
if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
iason_die("IASON_LEGE: invalid number");
while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos])) p->pos++;
frac_end = p->pos;
}
long exp = 0;
if (p->pos < p->len && (p->src[p->pos] == 'e' || p->src[p->pos] == 'E')) {
has_exp = 1;
p->pos++;
int esign = 1;
if (p->pos < p->len && (p->src[p->pos] == '+' || p->src[p->pos] == '-')) {
if (p->src[p->pos] == '-') esign = -1;
p->pos++;
}
if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
iason_die("IASON_LEGE: invalid number");
while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos])) {
exp = exp * 10 + (p->src[p->pos] - '0');
p->pos++;
}
exp *= esign;
}
size_t end = p->pos;
if (!has_frac && !has_exp) {
/* Pure integer. Use strtol-style parse (we already validated digits). */
long v = 0;
for (size_t i = (negative ? start + 1 : start); i < end; i++) {
v = v * 10 + (p->src[i] - '0');
}
return cent_int(negative ? -v : v);
}
if (!p->fractio) {
/* Floor to int. strtod handles the full grammar here. */
char *tmp = cent_arena_alloc(cent_arena, end - start + 1);
memcpy(tmp, p->src + start, end - start);
tmp[end - start] = '\0';
double d = strtod(tmp, NULL);
return cent_int((long)floor(d));
}
/* FRACTIO loaded: build an exact fraction from the decimal/exponent form. */
long num = 0;
/* Integer-part digits */
size_t int_start = negative ? start + 1 : start;
size_t int_end = has_frac ? (frac_start - 1) : (has_exp ? end - 0 : end);
/* If we have an exponent without a fraction part, find where digits end. */
if (has_exp && !has_frac) {
int_end = int_start;
while (int_end < p->len && isdigit((unsigned char)p->src[int_end])) int_end++;
}
for (size_t i = int_start; i < int_end; i++) {
num = num * 10 + (p->src[i] - '0');
}
/* Fractional digits, capped */
long den = 1;
if (has_frac) {
size_t take = frac_end - frac_start;
if (take > IASON_MAX_FRAC_DIGITS) take = IASON_MAX_FRAC_DIGITS;
for (size_t i = 0; i < take; i++) {
num = num * 10 + (p->src[frac_start + i] - '0');
den *= 10;
}
}
if (negative) num = -num;
/* Apply exponent: positive shifts num, negative shifts den. */
while (exp > 0) { num *= 10; exp--; }
while (exp < 0) { den *= 10; exp++; }
return iason_frac_reduce(num, den);
}
static CentValue iason_parse_value(IasonParser *p);
static CentValue iason_parse_array(IasonParser *p) {
iason_expect(p, '[', "IASON_LEGE: expected [");
iason_skip_ws(p);
CentValue lst = cent_list_new(4);
if (iason_peek(p) == ']') { p->pos++; return lst; }
for (;;) {
iason_skip_ws(p);
CentValue elem = iason_parse_value(p);
cent_list_push(&lst, elem);
iason_skip_ws(p);
int c = iason_peek(p);
if (c == ',') { p->pos++; continue; }
if (c == ']') { p->pos++; return lst; }
iason_die("IASON_LEGE: expected , or ] in array");
}
}
static CentValue iason_parse_object(IasonParser *p) {
iason_expect(p, '{', "IASON_LEGE: expected {");
iason_skip_ws(p);
CentValue d = cent_dict_new(4);
if (iason_peek(p) == '}') { p->pos++; return d; }
for (;;) {
iason_skip_ws(p);
if (iason_peek(p) != '"') iason_die("IASON_LEGE: object key must be a string");
char *key = iason_parse_string(p);
iason_skip_ws(p);
iason_expect(p, ':', "IASON_LEGE: expected : after object key");
iason_skip_ws(p);
CentValue val = iason_parse_value(p);
cent_dict_set(&d, cent_str(key), val);
iason_skip_ws(p);
int c = iason_peek(p);
if (c == ',') { p->pos++; continue; }
if (c == '}') { p->pos++; return d; }
iason_die("IASON_LEGE: expected , or } in object");
}
}
static CentValue iason_parse_value(IasonParser *p) {
iason_skip_ws(p);
int c = iason_peek(p);
if (c < 0) iason_die("IASON_LEGE: unexpected end of input");
if (c == '{') return iason_parse_object(p);
if (c == '[') return iason_parse_array(p);
if (c == '"') return cent_str(iason_parse_string(p));
if (c == 't') { iason_expect_word(p, "true"); return cent_bool(1); }
if (c == 'f') { iason_expect_word(p, "false"); return cent_bool(0); }
if (c == 'n') { iason_expect_word(p, "null"); return cent_null(); }
if (c == '-' || (c >= '0' && c <= '9')) return iason_parse_number(p);
iason_die("IASON_LEGE: unexpected character");
return cent_null();
}
CentValue cent_iason_lege(CentValue s, int fractio_loaded) {
if (s.type != CENT_STR) cent_type_error("IASON_LEGE requires a string");
IasonParser p = { s.sval, 0, strlen(s.sval), fractio_loaded };
CentValue v = iason_parse_value(&p);
iason_skip_ws(&p);
if (p.pos != p.len) iason_die("IASON_LEGE: trailing data after JSON value");
return v;
}
/* ---------- serializer --------------------------------------------- */
typedef struct {
char *buf;
size_t len;
size_t cap;
} IasonBuf;
static void iason_buf_reserve(IasonBuf *b, size_t extra) {
if (b->len + extra <= b->cap) return;
size_t new_cap = b->cap ? b->cap * 2 : 64;
while (new_cap < b->len + extra) new_cap *= 2;
char *nb = cent_arena_alloc(cent_arena, new_cap);
if (b->len) memcpy(nb, b->buf, b->len);
b->buf = nb;
b->cap = new_cap;
}
static void iason_buf_putc(IasonBuf *b, char c) {
iason_buf_reserve(b, 1);
b->buf[b->len++] = c;
}
static void iason_buf_puts(IasonBuf *b, const char *s) {
size_t n = strlen(s);
iason_buf_reserve(b, n);
memcpy(b->buf + b->len, s, n);
b->len += n;
}
static void iason_buf_putn(IasonBuf *b, const char *s, size_t n) {
iason_buf_reserve(b, n);
memcpy(b->buf + b->len, s, n);
b->len += n;
}
static void iason_emit_string(IasonBuf *b, const char *s) {
iason_buf_putc(b, '"');
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
unsigned char c = *p;
switch (c) {
case '"': iason_buf_puts(b, "\\\""); break;
case '\\': iason_buf_puts(b, "\\\\"); break;
case '\b': iason_buf_puts(b, "\\b"); break;
case '\f': iason_buf_puts(b, "\\f"); break;
case '\n': iason_buf_puts(b, "\\n"); break;
case '\r': iason_buf_puts(b, "\\r"); break;
case '\t': iason_buf_puts(b, "\\t"); break;
default:
if (c < 0x20) {
char tmp[8];
snprintf(tmp, sizeof tmp, "\\u%04x", c);
iason_buf_puts(b, tmp);
} else {
iason_buf_putc(b, (char)c);
}
}
}
iason_buf_putc(b, '"');
}
static void iason_emit_value(IasonBuf *b, CentValue v) {
switch (v.type) {
case CENT_NULL: iason_buf_puts(b, "null"); return;
case CENT_BOOL: iason_buf_puts(b, v.bval ? "true" : "false"); return;
case CENT_INT: {
char tmp[32];
int n = snprintf(tmp, sizeof tmp, "%ld", v.ival);
iason_buf_putn(b, tmp, (size_t)n);
return;
}
case CENT_FRAC: {
double d = (double)v.fval.num / (double)v.fval.den;
/* Shortest round-trippable representation, like Python's float repr. */
char tmp[64];
int n = 0;
for (int prec = 15; prec <= 17; prec++) {
n = snprintf(tmp, sizeof tmp, "%.*g", prec, d);
if (strtod(tmp, NULL) == d) break;
}
iason_buf_putn(b, tmp, (size_t)n);
return;
}
case CENT_STR: iason_emit_string(b, v.sval); return;
case CENT_LIST: {
iason_buf_putc(b, '[');
for (int i = 0; i < v.lval.len; i++) {
if (i > 0) iason_buf_puts(b, ", ");
iason_emit_value(b, v.lval.items[i]);
}
iason_buf_putc(b, ']');
return;
}
case CENT_DICT: {
iason_buf_putc(b, '{');
for (int i = 0; i < v.dval.len; i++) {
if (i > 0) iason_buf_puts(b, ", ");
CentValue k = v.dval.keys[i];
if (k.type != CENT_STR)
cent_runtime_error("IASON_SCRIBE: dict keys must be strings to serialize as JSON");
iason_emit_string(b, k.sval);
iason_buf_puts(b, ": ");
iason_emit_value(b, v.dval.vals[i]);
}
iason_buf_putc(b, '}');
return;
}
case CENT_FUNC:
cent_runtime_error("IASON_SCRIBE: cannot serialize a function");
return;
}
}
CentValue cent_iason_scribe(CentValue v) {
IasonBuf b = { NULL, 0, 0 };
iason_emit_value(&b, v);
iason_buf_putc(&b, '\0');
return cent_str(b.buf);
}

View File

@@ -0,0 +1,14 @@
#ifndef CENT_IASON_H
#define CENT_IASON_H
#include "cent_runtime.h"
/* IASON_LEGE — parse a JSON string into a CENTVRION value tree.
When fractio_loaded != 0, JSON floats become exact fractions; otherwise
they are floored to ints. */
CentValue cent_iason_lege(CentValue s, int fractio_loaded);
/* IASON_SCRIBE — serialize a CENTVRION value to a JSON string. */
CentValue cent_iason_scribe(CentValue v);
#endif /* CENT_IASON_H */

View File

@@ -15,6 +15,7 @@
CentArena *cent_arena;
int cent_magnvm = 0;
int cent_svbnvlla = 0;
/* ------------------------------------------------------------------ */
/* Portable xorshift32 RNG (matches Python _CentRng) */
@@ -34,6 +35,7 @@ static uint32_t cent_rng_next(void) {
jmp_buf _cent_try_stack[CENT_TRY_STACK_MAX];
int _cent_try_depth = 0;
const char *_cent_error_msg = NULL;
int _cent_current_line = 0;
/* ------------------------------------------------------------------ */
/* Arena allocator */
@@ -73,13 +75,20 @@ void *cent_arena_alloc(CentArena *a, size_t n) {
/* Error handling */
/* ------------------------------------------------------------------ */
static void _cent_die(const char *kind, const char *msg) {
if (_cent_current_line > 0)
fprintf(stderr, "CENTVRION %s: %s at line %d\n", kind, msg, _cent_current_line);
else
fprintf(stderr, "CENTVRION %s: %s\n", kind, msg);
exit(1);
}
void cent_type_error(const char *msg) {
if (_cent_try_depth > 0) {
_cent_error_msg = msg;
longjmp(_cent_try_stack[_cent_try_depth - 1], 1);
}
fprintf(stderr, "CENTVRION type error: %s\n", msg);
exit(1);
_cent_die("type error", msg);
}
void cent_runtime_error(const char *msg) {
@@ -87,8 +96,7 @@ void cent_runtime_error(const char *msg) {
_cent_error_msg = msg;
longjmp(_cent_try_stack[_cent_try_depth - 1], 1);
}
fprintf(stderr, "CENTVRION error: %s\n", msg);
exit(1);
_cent_die("error", msg);
}
/* ------------------------------------------------------------------ */
@@ -100,8 +108,11 @@ CentValue cent_scope_get(CentScope *s, const char *name) {
if (strcmp(s->names[i], name) == 0)
return s->vals[i];
}
fprintf(stderr, "CENTVRION error: undefined variable '%s'\n", name);
exit(1);
size_t bufsz = strlen(name) + 32;
char *buf = cent_arena_alloc(cent_arena, bufsz);
snprintf(buf, bufsz, "undefined variable '%s'", name);
cent_runtime_error(buf);
return cent_null(); /* unreachable */
}
void cent_scope_set(CentScope *s, const char *name, CentValue v) {
@@ -177,8 +188,20 @@ static void transform_thousands(const char *src, char *dst, size_t dstsz) {
}
void cent_int_to_roman(long n, char *buf, size_t bufsz) {
if (n <= 0 || (n > 3999 && !cent_magnvm))
cent_runtime_error("number out of range for Roman numerals (1-3999)");
if (n == 0) {
if (bufsz > 6) { memcpy(buf, "NVLLVS", 6); buf[6] = '\0'; }
return;
}
if (n < 0) {
if (!cent_svbnvlla)
cent_runtime_error("number out of range for Roman numerals");
if (bufsz < 2) cent_runtime_error("Roman numeral buffer overflow");
buf[0] = '-';
cent_int_to_roman(-n, buf + 1, bufsz - 1);
return;
}
if (n > 3999 && !cent_magnvm)
cent_runtime_error("number out of range for Roman numerals");
size_t pos = 0;
if (n > 3999) {
char base[64];
@@ -223,8 +246,10 @@ long cent_roman_to_int(const char *s) {
}
}
if (!matched) {
fprintf(stderr, "CENTVRION error: invalid Roman numeral: %s\n", s);
exit(1);
size_t bufsz = strlen(s) + 32;
char *buf = cent_arena_alloc(cent_arena, bufsz);
snprintf(buf, bufsz, "invalid Roman numeral: %s", s);
cent_runtime_error(buf);
}
}
return result;
@@ -267,8 +292,13 @@ static int write_val(CentValue v, char *buf, int bufsz) {
case CENT_FRAC: {
long num = v.fval.num, den = v.fval.den;
if (den < 0) { num = -num; den = -den; }
if (num < 0)
cent_runtime_error("cannot display negative numbers without SVBNVLLA");
int negative = 0;
if (num < 0) {
if (!cent_svbnvlla)
cent_runtime_error("cannot display negative numbers without SVBNVLLA");
negative = 1;
num = -num;
}
long int_part = num / den;
long rem_num = num % den;
@@ -293,11 +323,13 @@ static int write_val(CentValue v, char *buf, int bufsz) {
for (int i = 0; i < (int)((level_int % 6) % 2); i++) frac_buf[frac_pos++] = '.';
}
n = int_len + frac_pos;
n = int_len + frac_pos + (negative ? 1 : 0);
if (buf && n < bufsz) {
memcpy(buf, int_buf, int_len);
memcpy(buf + int_len, frac_buf, frac_pos);
buf[n] = '\0';
int pos = 0;
if (negative) buf[pos++] = '-';
memcpy(buf + pos, int_buf, int_len); pos += int_len;
memcpy(buf + pos, frac_buf, frac_pos); pos += frac_pos;
buf[pos] = '\0';
}
return n;
}
@@ -376,7 +408,9 @@ static long gcd(long a, long b) {
static CentValue frac_reduce(long num, long den) {
if (den < 0) { num = -num; den = -den; }
long g = gcd(num < 0 ? -num : num, den);
return cent_frac(num / g, den / g);
num /= g; den /= g;
if (den == 1) return cent_int(num);
return cent_frac(num, den);
}
static void to_frac(CentValue v, long *num, long *den) {
@@ -385,6 +419,13 @@ static void to_frac(CentValue v, long *num, long *den) {
else { *num = v.fval.num; *den = v.fval.den; }
}
CentValue cent_neg(CentValue v) {
if (v.type == CENT_INT) return cent_int(-v.ival);
if (v.type == CENT_FRAC) return frac_reduce(-v.fval.num, v.fval.den);
cent_type_error("Unary minus requires a number");
return cent_null();
}
CentValue cent_add(CentValue a, CentValue b) {
if (a.type == CENT_INT && b.type == CENT_INT)
return cent_int(a.ival + b.ival);
@@ -403,6 +444,18 @@ CentValue cent_add(CentValue a, CentValue b) {
return cent_null();
}
CentValue cent_array_concat(CentValue a, CentValue b) {
if (a.type != CENT_LIST || b.type != CENT_LIST)
cent_type_error("'@' requires two arrays");
int total = a.lval.len + b.lval.len;
CentValue result = cent_list_new(total);
for (int i = 0; i < a.lval.len; i++)
cent_list_push(&result, a.lval.items[i]);
for (int i = 0; i < b.lval.len; i++)
cent_list_push(&result, b.lval.items[i]);
return result;
}
CentValue cent_concat(CentValue a, CentValue b) {
const char *sa = (a.type == CENT_NULL) ? "" : cent_make_string(a);
const char *sb = (b.type == CENT_NULL) ? "" : cent_make_string(b);
@@ -441,11 +494,16 @@ CentValue cent_mul(CentValue a, CentValue b) {
}
CentValue cent_div(CentValue a, CentValue b) {
if (a.type == CENT_NULL) a = cent_int(0);
if (b.type == CENT_NULL) b = cent_int(0);
if (a.type != CENT_INT || b.type != CENT_INT)
cent_type_error("'/' requires two integers");
if (b.ival == 0)
cent_runtime_error("division by zero");
return cent_int(a.ival / b.ival);
/* floored division (Python // semantics) */
long q = a.ival / b.ival;
if ((a.ival % b.ival != 0) && ((a.ival < 0) != (b.ival < 0))) q -= 1;
return cent_int(q);
}
CentValue cent_div_frac(CentValue a, CentValue b) {
@@ -456,11 +514,16 @@ CentValue cent_div_frac(CentValue a, CentValue b) {
}
CentValue cent_mod(CentValue a, CentValue b) {
if (a.type == CENT_NULL) a = cent_int(0);
if (b.type == CENT_NULL) b = cent_int(0);
if (a.type != CENT_INT || b.type != CENT_INT)
cent_type_error("'RELIQVVM' requires two integers");
if (b.ival == 0)
cent_runtime_error("modulo by zero");
return cent_int(a.ival % b.ival);
/* floored modulo (Python % semantics) */
long r = a.ival % b.ival;
if (r != 0 && ((a.ival < 0) != (b.ival < 0))) r += b.ival;
return cent_int(r);
}
CentValue cent_mod_frac(CentValue a, CentValue b) {
@@ -480,6 +543,9 @@ CentValue cent_mod_frac(CentValue a, CentValue b) {
}
CentValue cent_eq(CentValue a, CentValue b) {
if ((a.type == CENT_INT && a.ival == 0 && b.type == CENT_NULL) ||
(a.type == CENT_NULL && b.type == CENT_INT && b.ival == 0))
return cent_bool(1);
if ((a.type == CENT_INT || a.type == CENT_FRAC) &&
(b.type == CENT_INT || b.type == CENT_FRAC)) {
long an, ad, bn, bd;
@@ -492,6 +558,14 @@ CentValue cent_eq(CentValue a, CentValue b) {
case CENT_BOOL: return cent_bool(a.bval == b.bval);
case CENT_FUNC: return cent_bool(a.fnval.fn == b.fnval.fn);
case CENT_NULL: return cent_bool(1);
case CENT_LIST: {
if (a.lval.len != b.lval.len) return cent_bool(0);
for (int i = 0; i < a.lval.len; i++) {
CentValue r = cent_eq(a.lval.items[i], b.lval.items[i]);
if (!r.bval) return cent_bool(0);
}
return cent_bool(1);
}
default:
cent_type_error("'EST' not supported for this type");
return cent_null();
@@ -525,6 +599,28 @@ CentValue cent_gt(CentValue a, CentValue b) {
return cent_null();
}
CentValue cent_lte(CentValue a, CentValue b) {
if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) &&
(b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return cent_bool(an * bd <= bn * ad);
}
cent_type_error("'HAVD_PLVS' requires two integers");
return cent_null();
}
CentValue cent_gte(CentValue a, CentValue b) {
if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) &&
(b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return cent_bool(an * bd >= bn * ad);
}
cent_type_error("'HAVD_MINVS' requires two integers");
return cent_null();
}
CentValue cent_and(CentValue a, CentValue b) {
if (a.type != CENT_BOOL || b.type != CENT_BOOL)
cent_type_error("'ET' requires two booleans");
@@ -569,6 +665,12 @@ CentValue cent_avdi_numerus(void) {
return cent_int(cent_roman_to_int(s.sval));
}
CentValue cent_numerus(CentValue s) {
if (s.type != CENT_STR)
cent_type_error("'NVMERVS' expects a string");
return cent_int(cent_roman_to_int(s.sval));
}
CentValue cent_longitudo(CentValue v) {
if (v.type == CENT_LIST) return cent_int(v.lval.len);
if (v.type == CENT_STR) return cent_int((long)strlen(v.sval));
@@ -577,6 +679,34 @@ CentValue cent_longitudo(CentValue v) {
return cent_null(); /* unreachable; silences warning */
}
CentValue cent_littera(CentValue v) {
return cent_str(cent_make_string(v));
}
CentValue cent_maivscvla(CentValue v) {
if (v.type != CENT_STR) cent_type_error("'MAIVSCVLA' requires a string");
size_t len = strlen(v.sval);
char *out = cent_arena_alloc(cent_arena, len + 1);
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)v.sval[i];
out[i] = (c >= 'a' && c <= 'z') ? (char)(c - ('a' - 'A')) : (char)c;
}
out[len] = '\0';
return cent_str(out);
}
CentValue cent_minvscvla(CentValue v) {
if (v.type != CENT_STR) cent_type_error("'MINVSCVLA' requires a string");
size_t len = strlen(v.sval);
char *out = cent_arena_alloc(cent_arena, len + 1);
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)v.sval[i];
out[i] = (c >= 'A' && c <= 'Z') ? (char)(c + ('a' - 'A')) : (char)c;
}
out[len] = '\0';
return cent_str(out);
}
CentValue cent_typvs(CentValue v) {
switch (v.type) {
case CENT_INT: return cent_str("NVMERVS");
@@ -730,6 +860,193 @@ CentValue cent_ordina(CentValue lst) {
return result;
}
/* User-comparator sort: single-threaded runtime, so the active comparator
and its calling scope are stashed in file-scope statics. Save/restore
them around the qsort call so nested ORDINA(..., cmp) calls inside a
comparator still work. */
static CentValue _cmp_active;
static CentScope _cmp_scope;
static int _ordina_user_comparator(const void *a, const void *b) {
const CentValue *va = (const CentValue *)a;
const CentValue *vb = (const CentValue *)b;
CentScope s1 = cent_scope_copy(&_cmp_scope);
cent_scope_set(&s1, _cmp_active.fnval.param_names[0], *va);
cent_scope_set(&s1, _cmp_active.fnval.param_names[1], *vb);
CentValue r1 = _cmp_active.fnval.fn(s1);
if (r1.type != CENT_BOOL)
cent_type_error("'ORDINA' comparator must return VERAX");
if (r1.bval) return -1;
CentScope s2 = cent_scope_copy(&_cmp_scope);
cent_scope_set(&s2, _cmp_active.fnval.param_names[0], *vb);
cent_scope_set(&s2, _cmp_active.fnval.param_names[1], *va);
CentValue r2 = _cmp_active.fnval.fn(s2);
if (r2.type != CENT_BOOL)
cent_type_error("'ORDINA' comparator must return VERAX");
return r2.bval ? 1 : 0;
}
CentValue cent_ordina_cmp(CentValue lst, CentValue cmp, CentScope scope) {
if (lst.type != CENT_LIST)
cent_type_error("'ORDINA' requires a list");
if (cmp.type != CENT_FUNC)
cent_type_error("'ORDINA' comparator must be a function");
if (cmp.fnval.param_count != 2)
cent_runtime_error("'ORDINA' comparator must take 2 arguments");
int len = lst.lval.len;
CentValue result = cent_list_new(len);
for (int i = 0; i < len; i++)
cent_list_push(&result, lst.lval.items[i]);
if (len > 1) {
CentValue saved_cmp = _cmp_active;
CentScope saved_scope = _cmp_scope;
_cmp_active = cmp;
_cmp_scope = scope;
qsort(result.lval.items, len, sizeof(CentValue), _ordina_user_comparator);
_cmp_active = saved_cmp;
_cmp_scope = saved_scope;
}
return result;
}
CentValue cent_mvta(CentValue lst, CentValue fn, CentScope scope) {
if (lst.type != CENT_LIST)
cent_type_error("'MVTA' requires an array");
if (fn.type != CENT_FUNC)
cent_type_error("'MVTA' requires a function");
if (fn.fnval.param_count != 1)
cent_runtime_error("'MVTA' function must take I argument");
int len = lst.lval.len;
CentValue result = cent_list_new(len);
for (int i = 0; i < len; i++) {
CentScope s = cent_scope_copy(&scope);
cent_scope_set(&s, fn.fnval.param_names[0], lst.lval.items[i]);
cent_list_push(&result, fn.fnval.fn(s));
}
return result;
}
CentValue cent_cribra(CentValue lst, CentValue fn, CentScope scope) {
if (lst.type != CENT_LIST)
cent_type_error("'CRIBRA' requires an array");
if (fn.type != CENT_FUNC)
cent_type_error("'CRIBRA' requires a function");
if (fn.fnval.param_count != 1)
cent_runtime_error("'CRIBRA' predicate must take I argument");
int len = lst.lval.len;
CentValue result = cent_list_new(len);
for (int i = 0; i < len; i++) {
CentScope s = cent_scope_copy(&scope);
cent_scope_set(&s, fn.fnval.param_names[0], lst.lval.items[i]);
CentValue r = fn.fnval.fn(s);
if (r.type != CENT_BOOL)
cent_type_error("'CRIBRA' predicate must return VERAX");
if (r.bval)
cent_list_push(&result, lst.lval.items[i]);
}
return result;
}
CentValue cent_confla(CentValue lst, CentValue init, CentValue fn, CentScope scope) {
if (lst.type != CENT_LIST)
cent_type_error("'CONFLA' requires an array");
if (fn.type != CENT_FUNC)
cent_type_error("'CONFLA' requires a function");
if (fn.fnval.param_count != 2)
cent_runtime_error("'CONFLA' function must take II arguments");
CentValue acc = init;
int len = lst.lval.len;
for (int i = 0; i < len; i++) {
CentScope s = cent_scope_copy(&scope);
cent_scope_set(&s, fn.fnval.param_names[0], acc);
cent_scope_set(&s, fn.fnval.param_names[1], lst.lval.items[i]);
acc = fn.fnval.fn(s);
}
return acc;
}
static long _index_arg(CentValue idx, const char *name) {
if (idx.type == CENT_INT)
return idx.ival;
if (idx.type == CENT_FRAC && idx.fval.den == 1)
return idx.fval.num;
cent_type_error(name);
return 0;
}
CentValue cent_adde(CentValue lst, CentValue v) {
if (lst.type != CENT_LIST)
cent_type_error("'ADDE' requires a list");
int len = lst.lval.len;
CentValue result = cent_list_new(len + 1);
for (int i = 0; i < len; i++)
cent_list_push(&result, lst.lval.items[i]);
cent_list_push(&result, v);
return result;
}
CentValue cent_tolle(CentValue lst, CentValue idx) {
if (lst.type != CENT_LIST)
cent_type_error("'TOLLE' requires a list");
long i = _index_arg(idx, "'TOLLE' index must be an integer");
int len = lst.lval.len;
if (i < 1 || i > len)
cent_runtime_error("'TOLLE' index out of range");
CentValue result = cent_list_new(len - 1);
for (int j = 0; j < len; j++)
if (j != i - 1)
cent_list_push(&result, lst.lval.items[j]);
return result;
}
CentValue cent_insere(CentValue lst, CentValue idx, CentValue v) {
if (lst.type != CENT_LIST)
cent_type_error("'INSERE' requires a list");
long i = _index_arg(idx, "'INSERE' index must be an integer");
int len = lst.lval.len;
if (i < 1 || i > len + 1)
cent_runtime_error("'INSERE' index out of range");
CentValue result = cent_list_new(len + 1);
for (int j = 0; j < i - 1; j++)
cent_list_push(&result, lst.lval.items[j]);
cent_list_push(&result, v);
for (int j = i - 1; j < len; j++)
cent_list_push(&result, lst.lval.items[j]);
return result;
}
CentValue cent_necte(CentValue a, CentValue b) {
if (a.type != CENT_LIST || b.type != CENT_LIST)
cent_type_error("'NECTE' requires two arrays");
if (a.lval.len != b.lval.len)
cent_runtime_error("'NECTE' requires arrays of equal length");
int len = a.lval.len;
CentValue result = cent_list_new(len);
for (int i = 0; i < len; i++) {
CentValue pair = cent_list_new(2);
cent_list_push(&pair, a.lval.items[i]);
cent_list_push(&pair, b.lval.items[i]);
cent_list_push(&result, pair);
}
return result;
}
CentValue cent_ivnge(CentValue keys, CentValue vals) {
if (keys.type != CENT_LIST || vals.type != CENT_LIST)
cent_type_error("'IVNGE' requires two arrays");
if (keys.lval.len != vals.lval.len)
cent_runtime_error("'IVNGE' requires arrays of equal length");
int len = keys.lval.len;
CentValue result = cent_dict_new(len);
for (int i = 0; i < len; i++) {
CentValue k = keys.lval.items[i];
if (k.type != CENT_INT && k.type != CENT_STR)
cent_runtime_error("Dict keys must be strings or integers");
cent_dict_set(&result, k, vals.lval.items[i]);
}
return result;
}
/* ------------------------------------------------------------------ */
/* Array helpers */
/* ------------------------------------------------------------------ */
@@ -819,8 +1136,23 @@ void cent_list_index_set(CentValue *lst, CentValue idx, CentValue v) {
cent_dict_set(lst, idx, v);
return;
}
if (lst->type == CENT_STR) {
if (idx.type != CENT_INT)
cent_type_error("string index must be an integer");
if (v.type != CENT_STR || strlen(v.sval) != 1)
cent_type_error("string index assignment requires a single character");
long slen = (long)strlen(lst->sval);
long i = idx.ival;
if (i < 1 || i > slen)
cent_runtime_error("string index out of range");
char *buf = cent_arena_alloc(cent_arena, slen + 1);
memcpy(buf, lst->sval, slen + 1);
buf[i - 1] = v.sval[0];
lst->sval = buf;
return;
}
if (lst->type != CENT_LIST)
cent_type_error("index-assign requires a list or dict");
cent_type_error("index-assign requires a list, dict, or string");
if (idx.type != CENT_INT)
cent_type_error("list index must be an integer");
long i = idx.ival;
@@ -840,44 +1172,123 @@ static int _cent_key_eq(CentValue a, CentValue b) {
return 0;
}
/* splitmix64 finalizer — good distribution for sequential ints */
static uint32_t _cent_hash_int(long v) {
uint64_t x = (uint64_t)v;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
x = x ^ (x >> 31);
return (uint32_t)x;
}
/* FNV-1a */
static uint32_t _cent_hash_str(const char *s) {
uint32_t h = 2166136261u;
for (; *s; s++) {
h ^= (uint8_t)*s;
h *= 16777619u;
}
return h;
}
static uint32_t _cent_hash_key(CentValue k) {
if (k.type == CENT_INT) return _cent_hash_int(k.ival);
if (k.type == CENT_STR) return _cent_hash_str(k.sval);
cent_type_error("dict key must be a numeral or string");
return 0;
}
static int _next_pow2(int n) {
int p = 1;
while (p < n) p <<= 1;
return p;
}
/* Probe for `key` in the bucket array. Returns the bucket slot — either
one whose stored index points to a matching key (hit), or an empty
slot (-1) where the key would be inserted. nbuckets is a power of 2. */
static int _cent_dict_probe(const CentDict *d, CentValue key, uint32_t h) {
uint32_t mask = (uint32_t)d->nbuckets - 1;
uint32_t i = h & mask;
while (1) {
int idx = d->buckets[i];
if (idx < 0) return (int)i;
if (_cent_key_eq(d->keys[idx], key)) return (int)i;
i = (i + 1) & mask;
}
}
static void _cent_dict_rehash(CentDict *d, int new_nbuckets) {
int *new_buckets = cent_arena_alloc(cent_arena, new_nbuckets * sizeof(int));
for (int i = 0; i < new_nbuckets; i++) new_buckets[i] = -1;
uint32_t mask = (uint32_t)new_nbuckets - 1;
for (int idx = 0; idx < d->len; idx++) {
uint32_t h = _cent_hash_key(d->keys[idx]);
uint32_t i = h & mask;
while (new_buckets[i] >= 0) i = (i + 1) & mask;
new_buckets[i] = idx;
}
d->buckets = new_buckets;
d->nbuckets = new_nbuckets;
}
CentValue cent_dict_new(int cap) {
if (cap < 4) cap = 4;
int nbuckets = _next_pow2(cap * 2);
CentValue *keys = cent_arena_alloc(cent_arena, cap * sizeof(CentValue));
CentValue *vals = cent_arena_alloc(cent_arena, cap * sizeof(CentValue));
return cent_dict_val(keys, vals, 0, cap);
int *buckets = cent_arena_alloc(cent_arena, nbuckets * sizeof(int));
for (int i = 0; i < nbuckets; i++) buckets[i] = -1;
return cent_dict_val(keys, vals, buckets, 0, cap, nbuckets);
}
void cent_dict_set(CentValue *dict, CentValue key, CentValue val) {
if (dict->type != CENT_DICT)
cent_type_error("dict-set requires a dict");
for (int i = 0; i < dict->dval.len; i++) {
if (_cent_key_eq(dict->dval.keys[i], key)) {
dict->dval.vals[i] = val;
return;
}
CentDict *d = &dict->dval;
uint32_t h = _cent_hash_key(key);
int slot = _cent_dict_probe(d, key, h);
int idx = d->buckets[slot];
if (idx >= 0) {
d->vals[idx] = val;
return;
}
if (dict->dval.len >= dict->dval.cap) {
int new_cap = dict->dval.cap * 2;
/* Grow the keys/vals arrays first so the new entry has a stable index. */
if (d->len >= d->cap) {
int new_cap = d->cap * 2;
CentValue *new_keys = cent_arena_alloc(cent_arena, new_cap * sizeof(CentValue));
CentValue *new_vals = cent_arena_alloc(cent_arena, new_cap * sizeof(CentValue));
memcpy(new_keys, dict->dval.keys, dict->dval.len * sizeof(CentValue));
memcpy(new_vals, dict->dval.vals, dict->dval.len * sizeof(CentValue));
dict->dval.keys = new_keys;
dict->dval.vals = new_vals;
dict->dval.cap = new_cap;
memcpy(new_keys, d->keys, d->len * sizeof(CentValue));
memcpy(new_vals, d->vals, d->len * sizeof(CentValue));
d->keys = new_keys;
d->vals = new_vals;
d->cap = new_cap;
}
int new_idx = d->len;
d->keys[new_idx] = key;
d->vals[new_idx] = val;
d->len++;
/* If load factor would exceed 0.75, rehash — this re-inserts every
entry including the one we just appended, so we're done. Otherwise
the slot picked by the earlier probe is still valid. */
if (d->len * 4 >= d->nbuckets * 3) {
_cent_dict_rehash(d, d->nbuckets * 2);
} else {
d->buckets[slot] = new_idx;
}
dict->dval.keys[dict->dval.len] = key;
dict->dval.vals[dict->dval.len] = val;
dict->dval.len++;
}
CentValue cent_dict_get(CentValue dict, CentValue key) {
if (dict.type != CENT_DICT)
cent_type_error("dict-get requires a dict");
for (int i = 0; i < dict.dval.len; i++) {
if (_cent_key_eq(dict.dval.keys[i], key))
return dict.dval.vals[i];
}
uint32_t h = _cent_hash_key(key);
int slot = _cent_dict_probe(&dict.dval, key, h);
int idx = dict.dval.buckets[slot];
if (idx >= 0) return dict.dval.vals[idx];
cent_runtime_error("Key not found in dict");
return cent_null();
}
@@ -895,11 +1306,160 @@ CentValue cent_dict_keys(CentValue dict) {
/* Regex */
/* ------------------------------------------------------------------ */
static int _is_roman_char(char c) {
return c == 'I' || c == 'V' || c == 'X' || c == 'L'
|| c == 'C' || c == 'D' || c == 'M';
}
static void _ensure_cap(char **out, size_t *opos, size_t *ocap, size_t need) {
while (*opos + need + 1 > *ocap) {
*ocap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, *ocap);
memcpy(newbuf, *out, *opos);
*out = newbuf;
}
}
/* Convert Roman numeral quantifiers in pattern: {III} → {3}, {II,V} → {2,5} */
static char *_romanize_pattern(const char *s) {
size_t slen = strlen(s);
size_t cap = slen * 2 + 1;
char *result = cent_arena_alloc(cent_arena, cap);
size_t rpos = 0;
for (size_t i = 0; i < slen; ) {
if (s[i] == '\\' && i + 1 < slen && _is_roman_char(s[i + 1])) {
/* backref: collect Roman numeral chars and convert */
size_t j = i + 1;
while (j < slen && _is_roman_char(s[j])) j++;
char buf[64];
size_t len = j - i - 1;
if (len >= sizeof(buf)) len = sizeof(buf) - 1;
memcpy(buf, s + i + 1, len);
buf[len] = '\0';
long val = cent_roman_to_int(buf);
char numbuf[32];
snprintf(numbuf, sizeof(numbuf), "\\%ld", val);
size_t nlen = strlen(numbuf);
while (rpos + nlen >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
memcpy(result + rpos, numbuf, nlen);
rpos += nlen;
i = j;
} else if (s[i] == '\\' && i + 1 < slen && s[i + 1] >= '0' && s[i + 1] <= '9') {
char msg[128];
snprintf(msg, sizeof(msg),
"Invalid escape sequence '\\%c' — use Roman numerals for backreferences", s[i + 1]);
cent_runtime_error(msg);
} else if (s[i] == '\\' && i + 1 < slen) {
if (rpos + 2 >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
result[rpos++] = s[i++];
result[rpos++] = s[i++];
} else if (s[i] == '[') {
/* copy character class verbatim */
if (rpos + 1 >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
result[rpos++] = s[i++];
if (i < slen && s[i] == '^') { result[rpos++] = s[i++]; }
if (i < slen && s[i] == ']') { result[rpos++] = s[i++]; }
while (i < slen && s[i] != ']') {
if (s[i] == '\\' && i + 1 < slen) {
if (rpos + 2 >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
result[rpos++] = s[i++];
}
if (rpos + 1 >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
result[rpos++] = s[i++];
}
if (i < slen) { if (rpos + 1 >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; } result[rpos++] = s[i++]; }
} else if (s[i] == '{') {
/* find closing brace */
size_t j = i + 1;
while (j < slen && s[j] != '}') j++;
if (j >= slen) {
if (rpos + 1 >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
result[rpos++] = s[i++];
} else {
/* extract inner content and try to convert */
size_t inner_len = j - i - 1;
char inner[128];
if (inner_len >= sizeof(inner)) inner_len = sizeof(inner) - 1;
memcpy(inner, s + i + 1, inner_len);
inner[inner_len] = '\0';
/* reject Arabic digit quantifiers */
int has_digit = 0, all_digit_comma_space = 1;
for (size_t k = 0; k < inner_len; k++) {
if (inner[k] >= '0' && inner[k] <= '9') has_digit = 1;
else if (inner[k] != ',' && inner[k] != ' ') all_digit_comma_space = 0;
}
if (has_digit && all_digit_comma_space) {
char msg[192];
snprintf(msg, sizeof(msg), "Invalid quantifier '{%s}' — use Roman numerals", inner);
cent_runtime_error(msg);
}
/* convert comma-separated Roman parts */
char converted[128];
size_t cpos = 0;
converted[0] = '\0';
int ok = 1;
char *part = inner;
while (ok) {
char *comma = strchr(part, ',');
if (comma) *comma = '\0';
/* trim spaces */
while (*part == ' ') part++;
char *pend = part + strlen(part) - 1;
while (pend > part && *pend == ' ') *pend-- = '\0';
if (*part == '\0') {
/* empty part (e.g. {,V}) */
} else {
/* check all chars are Roman */
int all_roman = 1;
for (char *c = part; *c; c++) { if (!_is_roman_char(*c)) { all_roman = 0; break; } }
if (!all_roman) { ok = 0; break; }
long val = cent_roman_to_int(part);
char numbuf[32];
snprintf(numbuf, sizeof(numbuf), "%ld", val);
size_t nlen = strlen(numbuf);
if (cpos + nlen >= sizeof(converted)) { ok = 0; break; }
memcpy(converted + cpos, numbuf, nlen);
cpos += nlen;
}
if (comma) {
if (cpos + 1 >= sizeof(converted)) { ok = 0; break; }
converted[cpos++] = ',';
part = comma + 1;
} else {
break;
}
}
converted[cpos] = '\0';
if (ok) {
size_t need = cpos + 2;
while (rpos + need >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
result[rpos++] = '{';
memcpy(result + rpos, converted, cpos);
rpos += cpos;
result[rpos++] = '}';
} else {
/* not valid Roman — copy verbatim */
size_t chunk = j - i + 1;
while (rpos + chunk >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
memcpy(result + rpos, s + i, chunk);
rpos += chunk;
}
i = j + 1;
}
} else {
if (rpos + 1 >= cap) { cap *= 2; char *nb = cent_arena_alloc(cent_arena, cap); memcpy(nb, result, rpos); result = nb; }
result[rpos++] = s[i++];
}
}
result[rpos] = '\0';
return result;
}
CentValue cent_qvaere(CentValue pattern, CentValue text) {
if (pattern.type != CENT_STR || text.type != CENT_STR)
cent_type_error("'QVAERE' requires two strings");
regex_t re;
int rc = regcomp(&re, pattern.sval, REG_EXTENDED);
int rc = regcomp(&re, _romanize_pattern(pattern.sval), REG_EXTENDED);
if (rc != 0) {
char errbuf[256];
regerror(rc, &re, errbuf, sizeof(errbuf));
@@ -922,42 +1482,39 @@ CentValue cent_qvaere(CentValue pattern, CentValue text) {
return result;
}
/* Expand replacement string, substituting \1..\9 with captured groups */
/* Expand replacement string, substituting \I..\IX with captured groups */
static void _expand_replacement(const char *repl, const char *subject,
regmatch_t *matches, int ngroups,
char **out, size_t *opos, size_t *ocap) {
for (const char *r = repl; *r; r++) {
if (*r == '\\' && r[1] >= '1' && r[1] <= '9') {
int g = r[1] - '0';
r++;
if (*r == '\\' && _is_roman_char(r[1])) {
const char *start = r + 1;
const char *end = start;
while (_is_roman_char(*end)) end++;
char buf[64];
size_t len = (size_t)(end - start);
if (len >= sizeof(buf)) len = sizeof(buf) - 1;
memcpy(buf, start, len);
buf[len] = '\0';
int g = (int)cent_roman_to_int(buf);
r = end - 1;
if (g < ngroups && matches[g].rm_so != -1) {
size_t glen = matches[g].rm_eo - matches[g].rm_so;
while (*opos + glen + 1 > *ocap) {
*ocap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, *ocap);
memcpy(newbuf, *out, *opos);
*out = newbuf;
}
_ensure_cap(out, opos, ocap, glen);
memcpy(*out + *opos, subject + matches[g].rm_so, glen);
*opos += glen;
}
} else if (*r == '\\' && r[1] >= '0' && r[1] <= '9') {
char msg[128];
snprintf(msg, sizeof(msg),
"Invalid escape sequence '\\%c' — use Roman numerals for backreferences", r[1]);
cent_runtime_error(msg);
} else if (*r == '\\' && r[1] == '\\') {
/* escaped backslash → literal \ */
if (*opos + 2 > *ocap) {
*ocap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, *ocap);
memcpy(newbuf, *out, *opos);
*out = newbuf;
}
_ensure_cap(out, opos, ocap, 1);
(*out)[(*opos)++] = '\\';
r++;
} else {
if (*opos + 2 > *ocap) {
*ocap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, *ocap);
memcpy(newbuf, *out, *opos);
*out = newbuf;
}
_ensure_cap(out, opos, ocap, 1);
(*out)[(*opos)++] = *r;
}
}
@@ -967,7 +1524,7 @@ CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue tex
if (pattern.type != CENT_STR || replacement.type != CENT_STR || text.type != CENT_STR)
cent_type_error("'SVBSTITVE' requires three strings");
regex_t re;
int rc = regcomp(&re, pattern.sval, REG_EXTENDED);
int rc = regcomp(&re, _romanize_pattern(pattern.sval), REG_EXTENDED);
if (rc != 0) {
char errbuf[256];
regerror(rc, &re, errbuf, sizeof(errbuf));
@@ -1015,6 +1572,40 @@ CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue tex
return cent_str(result);
}
CentValue cent_scinde(CentValue str, CentValue delim) {
if (str.type != CENT_STR || delim.type != CENT_STR)
cent_type_error("'SCINDE' requires two strings");
const char *s = str.sval;
const char *d = delim.sval;
size_t dlen = strlen(d);
CentValue result = cent_list_new(8);
if (dlen == 0) {
/* empty delimiter: split into individual characters */
for (const char *p = s; *p; p++) {
char *buf = cent_arena_alloc(cent_arena, 2);
buf[0] = *p;
buf[1] = '\0';
cent_list_push(&result, cent_str(buf));
}
return result;
}
const char *cursor = s;
for (;;) {
const char *found = strstr(cursor, d);
if (!found) {
cent_list_push(&result, cent_str(cursor));
break;
}
size_t len = found - cursor;
char *buf = cent_arena_alloc(cent_arena, len + 1);
memcpy(buf, cursor, len);
buf[len] = '\0';
cent_list_push(&result, cent_str(buf));
cursor = found + dlen;
}
return result;
}
/* ------------------------------------------------------------------ */
/* Networking (RETE) */
/* ------------------------------------------------------------------ */

View File

@@ -47,10 +47,13 @@ struct CentList {
};
struct CentDict {
CentValue *keys;
CentValue *vals;
int len;
int cap;
CentValue *keys; /* insertion-order array, len entries */
CentValue *vals; /* parallel to keys */
int *buckets; /* hash table; values are indices into */
/* keys/vals, or -1 for empty */
int len; /* number of entries */
int cap; /* capacity of keys/vals */
int nbuckets; /* size of buckets, power of 2 */
};
struct CentValue {
@@ -97,6 +100,9 @@ extern CentArena *cent_arena;
/* Set to 1 when CVM MAGNVM is active; enables extended numeral display */
extern int cent_magnvm;
/* Set to 1 when CVM SVBNVLLA is active; enables negative number display */
extern int cent_svbnvlla;
/* ------------------------------------------------------------------ */
/* Value constructors */
/* ------------------------------------------------------------------ */
@@ -132,13 +138,17 @@ static inline CentValue cent_func_val(CentFuncPtr fn, const char **param_names,
r.fnval.param_count = param_count;
return r;
}
static inline CentValue cent_dict_val(CentValue *keys, CentValue *vals, int len, int cap) {
static inline CentValue cent_dict_val(CentValue *keys, CentValue *vals,
int *buckets, int len, int cap,
int nbuckets) {
CentValue r;
r.type = CENT_DICT;
r.dval.keys = keys;
r.dval.vals = vals;
r.dval.len = len;
r.dval.cap = cap;
r.dval.keys = keys;
r.dval.vals = vals;
r.dval.buckets = buckets;
r.dval.len = len;
r.dval.cap = cap;
r.dval.nbuckets = nbuckets;
return r;
}
@@ -151,6 +161,9 @@ extern jmp_buf _cent_try_stack[];
extern int _cent_try_depth;
extern const char *_cent_error_msg;
/* Updated at the start of every emitted statement; 0 means "no line known". */
extern int _cent_current_line;
void cent_type_error(const char *msg); /* type mismatch → longjmp or exit(1) */
void cent_runtime_error(const char *msg); /* runtime fault → longjmp or exit(1) */
@@ -197,7 +210,9 @@ char *cent_make_string(CentValue v);
/* Arithmetic and comparison operators */
/* ------------------------------------------------------------------ */
CentValue cent_neg(CentValue v); /* unary minus: INT or FRAC */
CentValue cent_add(CentValue a, CentValue b); /* INT+INT or FRAC+FRAC/INT */
CentValue cent_array_concat(CentValue a, CentValue b); /* @ operator: concatenate two arrays */
CentValue cent_concat(CentValue a, CentValue b); /* & operator: coerce all types to str */
CentValue cent_sub(CentValue a, CentValue b); /* INT-INT or FRAC-FRAC/INT */
CentValue cent_mul(CentValue a, CentValue b); /* INT*INT or FRAC*FRAC/INT */
@@ -209,6 +224,8 @@ CentValue cent_eq (CentValue a, CentValue b); /* EST → BOOL */
CentValue cent_neq(CentValue a, CentValue b); /* DISPAR → BOOL */
CentValue cent_lt (CentValue a, CentValue b); /* MINVS → BOOL */
CentValue cent_gt (CentValue a, CentValue b); /* PLVS → BOOL */
CentValue cent_lte(CentValue a, CentValue b); /* HAVD_PLVS → BOOL */
CentValue cent_gte(CentValue a, CentValue b); /* HAVD_MINVS → BOOL */
CentValue cent_and(CentValue a, CentValue b); /* ET → BOOL */
CentValue cent_or (CentValue a, CentValue b); /* AVT → BOOL */
@@ -220,6 +237,9 @@ void cent_dic(CentValue v); /* DIC */
CentValue cent_avdi(void); /* AVDI */
CentValue cent_avdi_numerus(void); /* AVDI_NVMERVS */
CentValue cent_longitudo(CentValue v); /* LONGITVDO */
CentValue cent_littera(CentValue v); /* LITTERA */
CentValue cent_maivscvla(CentValue v); /* MAIVSCVLA */
CentValue cent_minvscvla(CentValue v); /* MINVSCVLA */
CentValue cent_fortuitus_numerus(CentValue lo, CentValue hi); /* FORTVITVS_NVMERVS */
CentValue cent_fortuita_electionis(CentValue lst); /* FORTVITA_ELECTIO */
CentValue cent_decimatio(CentValue lst); /* DECIMATIO */
@@ -229,11 +249,22 @@ CentValue cent_senatus(CentValue *args, int n); /* SENATVS */
CentValue cent_typvs(CentValue v); /* TYPVS */
void cent_dormi(CentValue n); /* DORMI */
CentValue cent_ordina(CentValue lst); /* ORDINA */
CentValue cent_ordina_cmp(CentValue lst, CentValue cmp, CentScope scope); /* ORDINA w/ comparator */
CentValue cent_mvta(CentValue lst, CentValue fn, CentScope scope); /* MVTA */
CentValue cent_cribra(CentValue lst, CentValue fn, CentScope scope); /* CRIBRA */
CentValue cent_confla(CentValue lst, CentValue init, CentValue fn, CentScope scope); /* CONFLA */
CentValue cent_adde(CentValue lst, CentValue v); /* ADDE */
CentValue cent_tolle(CentValue lst, CentValue idx); /* TOLLE */
CentValue cent_insere(CentValue lst, CentValue idx, CentValue v); /* INSERE */
CentValue cent_necte(CentValue a, CentValue b); /* NECTE */
CentValue cent_ivnge(CentValue keys, CentValue vals); /* IVNGE */
CentValue cent_lege(CentValue path); /* LEGE */
void cent_scribe(CentValue path, CentValue content); /* SCRIBE */
void cent_adivnge(CentValue path, CentValue content); /* ADIVNGE */
CentValue cent_numerus(CentValue s); /* NVMERVS */
CentValue cent_qvaere(CentValue pattern, CentValue text); /* QVAERE */
CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue text); /* SVBSTITVE */
CentValue cent_scinde(CentValue str, CentValue delim); /* SCINDE */
CentValue cent_pete(CentValue url); /* PETE */
void cent_petitvr(CentValue path, CentValue handler, CentScope scope); /* PETITVR */
void cent_avscvlta(CentValue port); /* AVSCVLTA */

View File

@@ -1 +1,13 @@
class CentvrionError(Exception): pass
class CentvrionError(Exception):
def __init__(self, msg, lineno=None, colno=None):
self.msg = msg
self.lineno = lineno
self.colno = colno
super().__init__(msg)
def __str__(self):
if self.lineno is None:
return self.msg
if self.colno is None:
return f"{self.msg} at line {self.lineno}"
return f"{self.msg} at line {self.lineno}, column {self.colno}"

View File

@@ -11,6 +11,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
"DEFINI",
"DESIGNA",
"DISPAR",
"DIVIDE",
"DONICVM",
"DVM",
"CONTINVA",
@@ -20,10 +21,14 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
"FAC",
"FALSITAS",
"FVNCTIO",
"GRADV",
"HAVD_MINVS",
"HAVD_PLVS",
"INVOCA",
"IN",
"MINVE",
"MINVS",
"MVLTIPLICA",
"NON",
"NVLLVS",
"PER",
@@ -41,28 +46,43 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
]]
builtin_tokens = [("BUILTIN", i) for i in [
"ADDE",
"AVDI_NVMERVS",
"AVDI",
"CLAVES",
"CONFLA",
"CRIBRA",
"DECIMATIO",
"DIC",
"DORMI",
"EVERRE",
"FORTVITVS_NVMERVS",
"FORTVITA_ELECTIO",
"INSERE",
"IVNGE",
"LITTERA",
"LONGITVDO",
"MAIVSCVLA",
"MINVSCVLA",
"MVTA",
"NECTE",
"NVMERVS",
"ORDINA",
"SEMEN",
"SENATVS",
"TOLLE",
"TYPVS",
"LEGE",
"SCRIBE",
"ADIVNGE",
"QVAERE",
"SVBSTITVE",
"SCINDE",
"PETE",
"PETITVR",
"AVSCVLTA"
"AVSCVLTA",
"IASON_LEGE",
"IASON_SCRIBE"
]]
data_tokens = [
@@ -74,6 +94,7 @@ data_tokens = [
module_tokens = [("MODULE", i) for i in [
"FORS",
"FRACTIO",
"IASON",
"MAGNVM",
"SCRIPTA",
"SVBNVLLA",
@@ -92,6 +113,7 @@ symbol_tokens = [
("SYMBOL_TIMES", r"\*"),
("SYMBOL_DIVIDE", r"\/"),
("SYMBOL_AMPERSAND", r"&"),
("SYMBOL_AT", r"@"),
("SYMBOL_COMMA", r",")
]
@@ -100,8 +122,8 @@ whitespace_tokens = [
]
all_tokens = (
keyword_tokens +
builtin_tokens +
keyword_tokens +
module_tokens +
data_tokens +
symbol_tokens +

View File

@@ -42,7 +42,30 @@ def _unescape(s):
return ''.join(out)
def _parse_interpolated(raw_value):
def _at(node, src):
"""Stamp a (lineno, colno) onto a freshly built AST node.
`src` can be an rply Token (uses .source_pos) or another Node (copies .pos).
"""
if src is None:
return node
pos = getattr(src, "pos", None)
if pos is not None:
node.pos = pos
return node
sp = getattr(src, "source_pos", None)
if sp is not None:
node.pos = (sp.lineno, sp.colno)
return node
def _parse_interpolated(raw_value, source_pos=None):
lineno = source_pos.lineno if source_pos is not None else None
colno = source_pos.colno if source_pos is not None else None
def _err(msg):
return CentvrionError(msg, lineno, colno)
quote_char = raw_value[0]
inner = raw_value[1:-1]
@@ -79,15 +102,15 @@ def _parse_interpolated(raw_value):
depth -= 1
j += 1
if depth != 0:
raise CentvrionError("Unclosed '{' in interpolated string")
raise _err("Unclosed '{' in interpolated string")
expr_src = inner[i + 1:j - 1]
tokens = Lexer().get_lexer().lex(expr_src + "\n")
program = Parser().parse(tokens)
if len(program.statements) != 1:
raise CentvrionError("Interpolation must contain exactly one expression")
raise _err("Interpolation must contain exactly one expression")
stmt = program.statements[0]
if not isinstance(stmt, ast_nodes.ExpressionStatement):
raise CentvrionError("Interpolation must contain an expression, not a statement")
raise _err("Interpolation must contain an expression, not a statement")
parts.append(stmt.expression)
i = j
elif ch == '}':
@@ -95,7 +118,7 @@ def _parse_interpolated(raw_value):
current.append('}')
i += 2
continue
raise CentvrionError("Unmatched '}' in string (use '}}' for literal '}')")
raise _err("Unmatched '}' in string (use '}}' for literal '}')")
else:
current.append(ch)
i += 1
@@ -115,8 +138,9 @@ class Parser():
precedence=[
('left', ["KEYWORD_AVT"]),
('left', ["KEYWORD_ET"]),
('left', ["KEYWORD_PLVS", "KEYWORD_MINVS", "KEYWORD_EST", "KEYWORD_DISPAR"]),
('left', ["SYMBOL_AMPERSAND", "SYMBOL_PLUS", "SYMBOL_MINUS"]),
('left', ["KEYWORD_PLVS", "KEYWORD_MINVS", "KEYWORD_EST", "KEYWORD_DISPAR",
"KEYWORD_HAVD_PLVS", "KEYWORD_HAVD_MINVS"]),
('left', ["SYMBOL_AMPERSAND", "SYMBOL_AT", "SYMBOL_PLUS", "SYMBOL_MINUS"]),
('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE", "KEYWORD_RELIQVVM"]),
('right', ["UMINUS", "UNOT"]),
('left', ["SYMBOL_LBRACKET", "INDEX"]),
@@ -153,7 +177,7 @@ class Parser():
@self.pg.production('module_call : KEYWORD_CVM MODULE')
def module_call(tokens):
return ast_nodes.ModuleCall(tokens[1].value)
return _at(ast_nodes.ModuleCall(tokens[1].value), tokens[0])
# Statements
@@ -171,35 +195,51 @@ class Parser():
@self.pg.production('statement : KEYWORD_DESIGNA id KEYWORD_VT expression')
def statement_designa(tokens):
return ast_nodes.Designa(tokens[1], tokens[3])
return _at(ast_nodes.Designa(tokens[1], tokens[3]), tokens[0])
@self.pg.production('statement : KEYWORD_DESIGNA id SYMBOL_LBRACKET expression SYMBOL_RBRACKET KEYWORD_VT expression')
@self.pg.production('index_chain : SYMBOL_LBRACKET expression SYMBOL_RBRACKET')
def index_chain_single(tokens):
return [tokens[1]]
@self.pg.production('index_chain : SYMBOL_LBRACKET expression SYMBOL_RBRACKET index_chain')
def index_chain_multi(tokens):
return [tokens[1]] + tokens[3]
@self.pg.production('statement : KEYWORD_DESIGNA id index_chain KEYWORD_VT expression')
def statement_designa_index(tokens):
return ast_nodes.DesignaIndex(tokens[1], tokens[3], tokens[6])
return _at(ast_nodes.DesignaIndex(tokens[1], tokens[2], tokens[4]), tokens[0])
@self.pg.production('statement : KEYWORD_DESIGNA id SYMBOL_COMMA id_list_rest KEYWORD_VT expression')
def statement_designa_destructure(tokens):
return ast_nodes.DesignaDestructure([tokens[1]] + tokens[3], tokens[5])
return _at(ast_nodes.DesignaDestructure([tokens[1]] + tokens[3], tokens[5]), tokens[0])
@self.pg.production('statement : id KEYWORD_AVGE expression')
def statement_avge(tokens):
return ast_nodes.Designa(tokens[0], ast_nodes.BinOp(tokens[0], tokens[2], "SYMBOL_PLUS"))
return _at(ast_nodes.Designa(tokens[0], ast_nodes.BinOp(tokens[0], tokens[2], "SYMBOL_PLUS")), tokens[0])
@self.pg.production('statement : id KEYWORD_MINVE expression')
def statement_minve(tokens):
return ast_nodes.Designa(tokens[0], ast_nodes.BinOp(tokens[0], tokens[2], "SYMBOL_MINUS"))
return _at(ast_nodes.Designa(tokens[0], ast_nodes.BinOp(tokens[0], tokens[2], "SYMBOL_MINUS")), tokens[0])
@self.pg.production('statement : id KEYWORD_MVLTIPLICA expression')
def statement_mvltiplica(tokens):
return _at(ast_nodes.Designa(tokens[0], ast_nodes.BinOp(tokens[0], tokens[2], "SYMBOL_TIMES")), tokens[0])
@self.pg.production('statement : id KEYWORD_DIVIDE expression')
def statement_divide(tokens):
return _at(ast_nodes.Designa(tokens[0], ast_nodes.BinOp(tokens[0], tokens[2], "SYMBOL_DIVIDE")), tokens[0])
@self.pg.production('statement : expression')
def statement_expression(tokens):
return ast_nodes.ExpressionStatement(tokens[0])
return _at(ast_nodes.ExpressionStatement(tokens[0]), tokens[0])
@self.pg.production('statement : KEYWORD_DEFINI id ids KEYWORD_VT SYMBOL_LCURL statements SYMBOL_RCURL')
def defini(tokens):
return ast_nodes.Defini(tokens[1], tokens[2], tokens[5])
return _at(ast_nodes.Defini(tokens[1], tokens[2], tokens[5]), tokens[0])
@self.pg.production('statement : KEYWORD_REDI expressions')
def redi(tokens):
return ast_nodes.Redi(tokens[1])
return _at(ast_nodes.Redi(tokens[1]), tokens[0])
@self.pg.production('statement : per_statement')
@self.pg.production('statement : dum_statement')
@@ -210,20 +250,20 @@ class Parser():
return tokens[0]
@self.pg.production('statement : KEYWORD_ERVMPE')
def erumpe(_):
return ast_nodes.Erumpe()
def erumpe(tokens):
return _at(ast_nodes.Erumpe(), tokens[0])
@self.pg.production('statement : KEYWORD_CONTINVA')
def continva(_):
return ast_nodes.Continva()
def continva(tokens):
return _at(ast_nodes.Continva(), tokens[0])
@self.pg.production('si_statement : KEYWORD_SI expression KEYWORD_TVNC SYMBOL_LCURL statements SYMBOL_RCURL')
@self.pg.production('si_statement : KEYWORD_SI expression KEYWORD_TVNC SYMBOL_LCURL statements SYMBOL_RCURL aluid_statement')
def si_statement(tokens):
if len(tokens) == 7:
return ast_nodes.SiStatement(tokens[1], tokens[4], tokens[6])
return _at(ast_nodes.SiStatement(tokens[1], tokens[4], tokens[6]), tokens[0])
else:
return ast_nodes.SiStatement(tokens[1], tokens[4], None)
return _at(ast_nodes.SiStatement(tokens[1], tokens[4], None), tokens[0])
@self.pg.production('aluid_statement : KEYWORD_ALIVD si_statement')
def aluid_si(tokens):
@@ -235,25 +275,34 @@ class Parser():
@self.pg.production('dum_statement : KEYWORD_DVM expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
def dum(tokens):
return ast_nodes.DumStatement(tokens[1], tokens[4])
return _at(ast_nodes.DumStatement(tokens[1], tokens[4]), tokens[0])
# AETERNVM is sugar for `DVM FALSITAS` — same AST, no observable difference.
@self.pg.production('dum_statement : KEYWORD_AETERNVM KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
def aeternvm(tokens):
return ast_nodes.DumStatement(ast_nodes.Bool(False), tokens[3])
return _at(ast_nodes.DumStatement(ast_nodes.Bool(False), tokens[3]), tokens[0])
@self.pg.production('per_statement : KEYWORD_PER id SYMBOL_COMMA id_list_rest KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
def per_destructure(tokens):
return _at(ast_nodes.PerStatement(tokens[5], [tokens[1]] + tokens[3], tokens[8]), tokens[0])
@self.pg.production('per_statement : KEYWORD_PER id KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
def per(tokens):
return ast_nodes.PerStatement(tokens[3], tokens[1], tokens[6])
return _at(ast_nodes.PerStatement(tokens[3], tokens[1], tokens[6]), tokens[0])
@self.pg.production('tempta_statement : KEYWORD_TEMPTA SYMBOL_LCURL statements SYMBOL_RCURL KEYWORD_CAPE id SYMBOL_LCURL statements SYMBOL_RCURL')
def tempta(tokens):
return ast_nodes.TemptaStatement(tokens[2], tokens[5], tokens[7])
return _at(ast_nodes.TemptaStatement(tokens[2], tokens[5], tokens[7]), tokens[0])
@self.pg.production('donicum_statement : KEYWORD_DONICVM id KEYWORD_VT expression KEYWORD_VSQVE expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
def donicum(tokens):
range_array = ast_nodes.DataRangeArray(tokens[3], tokens[5])
return ast_nodes.PerStatement(range_array, tokens[1], tokens[8])
range_array = _at(ast_nodes.DataRangeArray(tokens[3], tokens[5]), tokens[0])
return _at(ast_nodes.PerStatement(range_array, tokens[1], tokens[8]), tokens[0])
@self.pg.production('donicum_statement : KEYWORD_DONICVM id KEYWORD_VT expression KEYWORD_VSQVE expression KEYWORD_GRADV expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
def donicum_step(tokens):
range_array = _at(ast_nodes.DataRangeArray(tokens[3], tokens[5], tokens[7]), tokens[0])
return _at(ast_nodes.PerStatement(range_array, tokens[1], tokens[10]), tokens[0])
# expressions
@self.pg.production('expressions : SYMBOL_LPARENS expression_list')
@@ -271,16 +320,13 @@ class Parser():
else:
return [calls[0]] + calls[2]
@self.pg.production('array_items : ')
@self.pg.production('array_items : expression')
@self.pg.production('array_items : expression SYMBOL_COMMA array_items')
@self.pg.production('array_items : expression SYMBOL_COMMA opt_newline array_items')
def array_items(calls):
if len(calls) == 0:
return []
elif len(calls) == 1:
if len(calls) == 1:
return [calls[0]]
else:
return [calls[0]] + calls[2]
return [calls[0]] + calls[3]
@self.pg.production('expression : id')
def expression_id(tokens):
@@ -288,29 +334,31 @@ class Parser():
@self.pg.production('expression : BUILTIN expressions')
def expression_builtin(tokens):
return ast_nodes.BuiltIn(tokens[0].value, tokens[1])
return _at(ast_nodes.BuiltIn(tokens[0].value, tokens[1]), tokens[0])
@self.pg.production('expression : DATA_STRING')
def expression_string(tokens):
return _parse_interpolated(tokens[0].value)
node = _parse_interpolated(tokens[0].value, tokens[0].source_pos)
return _at(node, tokens[0])
@self.pg.production('expression : DATA_NUMERAL')
def expression_numeral(tokens):
return ast_nodes.Numeral(tokens[0].value)
return _at(ast_nodes.Numeral(tokens[0].value), tokens[0])
@self.pg.production('expression : DATA_FRACTION')
def expression_fraction(tokens):
return ast_nodes.Fractio(tokens[0].value)
return _at(ast_nodes.Fractio(tokens[0].value), tokens[0])
@self.pg.production('expression : KEYWORD_FALSITAS')
@self.pg.production('expression : KEYWORD_VERITAS')
def expression_bool(tokens):
return ast_nodes.Bool(tokens[0].name == "KEYWORD_VERITAS")
return _at(ast_nodes.Bool(tokens[0].name == "KEYWORD_VERITAS"), tokens[0])
@self.pg.production('expression : KEYWORD_NVLLVS')
def expression_nullus(_):
return ast_nodes.Nullus()
def expression_nullus(tokens):
return _at(ast_nodes.Nullus(), tokens[0])
@self.pg.production('expression : expression SYMBOL_AT expression')
@self.pg.production('expression : expression SYMBOL_AMPERSAND expression')
@self.pg.production('expression : expression SYMBOL_MINUS expression')
@self.pg.production('expression : expression SYMBOL_PLUS expression')
@@ -321,61 +369,77 @@ class Parser():
@self.pg.production('expression : expression KEYWORD_DISPAR expression')
@self.pg.production('expression : expression KEYWORD_MINVS expression')
@self.pg.production('expression : expression KEYWORD_PLVS expression')
@self.pg.production('expression : expression KEYWORD_HAVD_PLVS expression')
@self.pg.production('expression : expression KEYWORD_HAVD_MINVS expression')
@self.pg.production('expression : expression KEYWORD_ET expression')
@self.pg.production('expression : expression KEYWORD_AVT expression')
def binop(tokens):
return ast_nodes.BinOp(tokens[0], tokens[2], tokens[1].name)
return _at(ast_nodes.BinOp(tokens[0], tokens[2], tokens[1].name), tokens[0])
@self.pg.production('expression : SYMBOL_MINUS expression', precedence='UMINUS')
def unary_minus(tokens):
return ast_nodes.UnaryMinus(tokens[1])
return _at(ast_nodes.UnaryMinus(tokens[1]), tokens[0])
@self.pg.production('expression : KEYWORD_NON expression', precedence='UNOT')
def unary_not(tokens):
return ast_nodes.UnaryNot(tokens[1])
return _at(ast_nodes.UnaryNot(tokens[1]), tokens[0])
@self.pg.production('expression : KEYWORD_INVOCA expression expressions')
def invoca(tokens):
return ast_nodes.Invoca(tokens[1], tokens[2])
return _at(ast_nodes.Invoca(tokens[1], tokens[2]), tokens[0])
@self.pg.production('expression : KEYWORD_FVNCTIO ids KEYWORD_VT SYMBOL_LCURL statements SYMBOL_RCURL')
def fvnctio(tokens):
return ast_nodes.Fvnctio(tokens[1], tokens[4])
return _at(ast_nodes.Fvnctio(tokens[1], tokens[4]), tokens[0])
@self.pg.production('expression : SYMBOL_LPARENS expression SYMBOL_RPARENS')
def parens(tokens):
return tokens[1]
@self.pg.production('dict_items : ')
@self.pg.production('dict_items : expression KEYWORD_VT expression')
@self.pg.production('dict_items : expression KEYWORD_VT expression SYMBOL_COMMA dict_items')
@self.pg.production('dict_items : expression KEYWORD_VT expression SYMBOL_COMMA opt_newline dict_items')
def dict_items(calls):
if len(calls) == 0:
return []
elif len(calls) == 3:
if len(calls) == 3:
return [(calls[0], calls[2])]
else:
return [(calls[0], calls[2])] + calls[4]
return [(calls[0], calls[2])] + calls[5]
@self.pg.production('expression : KEYWORD_TABVLA SYMBOL_LCURL dict_items SYMBOL_RCURL')
@self.pg.production('expression : KEYWORD_TABVLA SYMBOL_LCURL opt_newline SYMBOL_RCURL')
def dict_literal_empty(tokens):
return _at(ast_nodes.DataDict([]), tokens[0])
@self.pg.production('expression : KEYWORD_TABVLA SYMBOL_LCURL opt_newline dict_items opt_newline SYMBOL_RCURL')
def dict_literal(tokens):
return ast_nodes.DataDict(tokens[2])
return _at(ast_nodes.DataDict(tokens[3]), tokens[0])
@self.pg.production('expression : SYMBOL_LBRACKET array_items SYMBOL_RBRACKET')
@self.pg.production('expression : SYMBOL_LBRACKET SYMBOL_RBRACKET')
@self.pg.production('expression : SYMBOL_LBRACKET newlines SYMBOL_RBRACKET')
def array_empty(tokens):
return _at(ast_nodes.DataArray([]), tokens[0])
@self.pg.production('expression : SYMBOL_LBRACKET array_items opt_newline SYMBOL_RBRACKET')
def array(tokens):
return ast_nodes.DataArray(tokens[1])
return _at(ast_nodes.DataArray(tokens[1]), tokens[0])
@self.pg.production('expression : SYMBOL_LBRACKET newlines array_items opt_newline SYMBOL_RBRACKET')
def array_leading_newline(tokens):
return _at(ast_nodes.DataArray(tokens[2]), tokens[0])
@self.pg.production('expression : SYMBOL_LBRACKET expression KEYWORD_VSQVE expression SYMBOL_RBRACKET')
def range_array(tokens):
return ast_nodes.DataRangeArray(tokens[1], tokens[3])
return _at(ast_nodes.DataRangeArray(tokens[1], tokens[3]), tokens[0])
@self.pg.production('expression : SYMBOL_LBRACKET expression KEYWORD_VSQVE expression KEYWORD_GRADV expression SYMBOL_RBRACKET')
def range_array_step(tokens):
return _at(ast_nodes.DataRangeArray(tokens[1], tokens[3], tokens[5]), tokens[0])
@self.pg.production('expression : expression SYMBOL_LBRACKET expression SYMBOL_RBRACKET', precedence='INDEX')
def array_index(tokens):
return ast_nodes.ArrayIndex(tokens[0], tokens[2])
return _at(ast_nodes.ArrayIndex(tokens[0], tokens[2]), tokens[0])
@self.pg.production('expression : expression SYMBOL_LBRACKET expression KEYWORD_VSQVE expression SYMBOL_RBRACKET', precedence='INDEX')
def array_slice(tokens):
return ast_nodes.ArraySlice(tokens[0], tokens[2], tokens[4])
return _at(ast_nodes.ArraySlice(tokens[0], tokens[2], tokens[4]), tokens[0])
# ids
@self.pg.production('ids : SYMBOL_LPARENS id_list')
@@ -403,11 +467,17 @@ class Parser():
@self.pg.production("id : ID")
def id_expression(tokens):
return ast_nodes.ID(tokens[0].value)
return _at(ast_nodes.ID(tokens[0].value), tokens[0])
@self.pg.error
def error_handle(token):
raise SyntaxError(f"{token.name}, {token.value}, {token.source_pos}")
pos = token.source_pos
loc = f" at line {pos.lineno}, column {pos.colno}" if pos else ""
if token.name == "SYMBOL_LPARENS":
raise SyntaxError(
f"Unexpected '('{loc}. To call a function, use INVOCA: INVOCA func (args)"
)
raise SyntaxError(f"Unexpected token '{token.value}'{loc}")
parser = self.pg.build()
return parser.parse(tokens_input) # type: ignore

Binary file not shown.

View File

@@ -33,12 +33,15 @@
\languageline{statement}{\texttt{DESIGNA} \textbf{id} \texttt{,} \textit{ids} \texttt{VT} \textit{expression}} \\
\languageline{statement}{\textbf{id} \texttt{AVGE} \textit{expression}} \\
\languageline{statement}{\textbf{id} \texttt{MINVE} \textit{expression}} \\
\languageline{statement}{\textbf{id} \texttt{MVLTIPLICA} \textit{expression}} \\
\languageline{statement}{\textbf{id} \texttt{DIVIDE} \textit{expression}} \\
\languageline{statement}{\texttt{DEFINI} \textbf{id} \texttt{(} \textit{optional-ids} \texttt{)} \texttt{VT} \textit{scope}} \\
\languageline{statement}{\textit{if-statement}} \\
\languageline{statement}{\texttt{DVM} \textit{expression} \texttt{FAC} \textit{scope}} \\
\languageline{statement}{\texttt{AETERNVM} \texttt{FAC} \textit{scope}} \\
\languageline{statement}{\texttt{PER} \textbf{id} \texttt{IN} \textit{expression} \texttt{FAC} \textit{scope}} \\
\languageline{statement}{\texttt{DONICVM} \textbf{id} \texttt{VT} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{FAC} \textit{scope}} \\
\languageline{statement}{\texttt{PER} \textbf{id}\texttt{,} \textbf{id-list} \texttt{IN} \textit{expression} \texttt{FAC} \textit{scope}} \\
\languageline{statement}{\texttt{DONICVM} \textbf{id} \texttt{VT} \textit{expression} \texttt{VSQVE} \textit{expression} \textit{optional-step} \texttt{FAC} \textit{scope}} \\
\languageline{statement}{\texttt{REDI(} \textit{optional-expressions} \texttt{)}} \\
\languageline{statement}{\texttt{ERVMPE}} \\
\languageline{statement}{\texttt{CONTINVA}} \\
@@ -70,7 +73,7 @@
\languageline{literal}{\textbf{numeral}} \\
\languageline{literal}{\textbf{bool}} \\
\languageline{literal}{\texttt{[} \textit{optional-expressions} \texttt{]}} \\
\languageline{literal}{\texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{]} \textnormal{\small\ (inclusive on both ends)}} \\
\languageline{literal}{\texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \textit{optional-step} \texttt{]} \textnormal{\small\ (inclusive on both ends)}} \\
\languageline{literal}{\texttt{TABVLA} \texttt{\{} \textit{optional-dict-items} \texttt{\}}} \\ \hline
\languageline{optional-dict-items}{\textit{dict-items}} \\
@@ -91,6 +94,9 @@
\languageline{expressions}{\textit{expression}, \textit{expressions}} \\
\languageline{expressions}{\textit{expression}} \\ \hline
\languageline{optional-step}{\texttt{GRADV} \textit{expression}} \\
\languageline{optional-step}{} \\ \hline
\end{tabular}
\end{center}
\end{table}
@@ -98,14 +104,14 @@
\newpage
\begin{itemize}
\item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple.
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features. Available modules: \texttt{FORS} (randomness), \texttt{FRACTIO} (fractions), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals), \texttt{RETE} (networking: \texttt{PETE}, \texttt{PETITVR}, \texttt{AVSCVLTA}).
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features. Available modules: \texttt{FORS} (randomness), \texttt{FRACTIO} (fractions), \texttt{IASON} (JSON I/O: \texttt{IASON\_LEGE}, \texttt{IASON\_SCRIBE}), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals), \texttt{RETE} (networking: \texttt{PETE}, \texttt{PETITVR}, \texttt{AVSCVLTA}).
\item \textbf{id}: \\ Variable. Can only consist of lowercase characters and underscores, but not the letters j, u, or w.
\item \textbf{builtin}: \\ Builtin functions are uppercase latin words.
\item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal. Strings support 1-based indexing (\texttt{string[I]}) and inclusive slicing (\texttt{string[I VSQVE III]}), returning single-character strings and substrings respectively.
\item \textbf{interpolated-string}: \\ A double-quoted string containing \texttt{\{}\textit{expression}\texttt{\}} segments. Each expression is evaluated and coerced to a string. Use \texttt{\{\{} and \texttt{\}\}} for literal braces.
\item \textbf{numeral}: \\ Roman numerals consisting of the uppercase characters I, V, X, L, C, D, and M. Can also include underscore if the module MAGNVM.
\item \textbf{bool}: \\ VERITAS or FALSITAS.
\item \textbf{binop}: \\ Binary operators: \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{RELIQVVM} (modulo), \texttt{EST} (equality), \texttt{DISPAR} (not-equal), \texttt{MINVS} (<), \texttt{PLVS} (>), \texttt{ET} (and), \texttt{AVT} (or), \texttt{\&} (string concatenation).
\item \textbf{binop}: \\ Binary operators: \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{RELIQVVM} (modulo), \texttt{EST} (equality), \texttt{DISPAR} (not-equal), \texttt{MINVS} (<), \texttt{PLVS} (>), \texttt{HAVD\_PLVS} ($\leq$), \texttt{HAVD\_MINVS} ($\geq$), \texttt{ET} (and), \texttt{AVT} (or), \texttt{\&} (string concatenation), \texttt{@} (array concatenation).
\item \textbf{unop}: \\ Unary operators: \texttt{-} (negation), \texttt{NON} (boolean not).
\end{itemize}

3
pyproject.toml Normal file
View File

@@ -0,0 +1,3 @@
[tool.pytest.ini_options]
python_files = ["[0-9][0-9]_test_*.py", "test_*.py"]
testpaths = ["tests"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,2 @@
DESIGNA x VT [I, II, III] @ [IV, V]
DIC(x)

BIN
snippets/array_concat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,3 +1,6 @@
DESIGNA x VT V
x AVGE III
x MINVE II
x MVLTIPLICA IV
x DIVIDE III
DIC(x)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 19 KiB

4
snippets/confla.cent Normal file
View File

@@ -0,0 +1,4 @@
DEFINI addi (a, b) VT {
REDI (a + b)
}
DIC (CONFLA([I, II, III, IV, V], I, addi))

BIN
snippets/confla.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

3
snippets/cribra.cent Normal file
View File

@@ -0,0 +1,3 @@
DIC (CRIBRA([I, II, III, IV, V, VI], FVNCTIO (x) VT {
REDI (x HAVD_PLVS III)
}))

BIN
snippets/cribra.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,5 @@
DESIGNA s VT NVLLVS
DONICVM i VT I VSQVE X GRADV II FAC {
s AVGE i
}
DIC(s)

BIN
snippets/donicvm_gradv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

1
snippets/iason.cent Normal file
View File

@@ -0,0 +1 @@
CVM IASON

BIN
snippets/iason.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

5
snippets/iason_lege.cent Normal file
View File

@@ -0,0 +1,5 @@
CVM IASON
DESIGNA data VT IASON_LEGE('{"nomen": "Marcus", "anni": 30, "armorum": ["gladius", "scutum"]}')
DIC(data["nomen"])
DIC(data["anni"])
DIC(data["armorum"])

BIN
snippets/iason_lege.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,3 @@
CVM IASON
DESIGNA persona VT TABVLA {"nomen" VT "Marcus", "anni" VT XXX}
DIC(IASON_SCRIBE(persona))

BIN
snippets/iason_scribe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 87 KiB

3
snippets/littera.cent Normal file
View File

@@ -0,0 +1,3 @@
DESIGNA n VT VII
DESIGNA s VT LITTERA(n) & " est septem"
DIC(s)

BIN
snippets/littera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

4
snippets/mvta.cent Normal file
View File

@@ -0,0 +1,4 @@
DEFINI dbl (x) VT {
REDI (x + x)
}
DIC (MVTA([I, II, III, IV], dbl))

BIN
snippets/mvta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

4
snippets/ordina_cmp.cent Normal file
View File

@@ -0,0 +1,4 @@
DEFINI gt (a, b) VT {
REDI (a PLVS b)
}
DIC (ORDINA([II, V, I, III], gt))

BIN
snippets/ordina_cmp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
PER a, b IN [[I, II], [III, IV]] FAC {
DIC(a + b)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,2 @@
DIC("SALVTE"[I])
DIC("SALVTE"[III])

BIN
snippets/string_index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1 @@
DIC("SALVTE"[II VSQVE IV])

BIN
snippets/string_slice.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -10,11 +10,11 @@ contexts:
- include: comments
- include: strings
- include: keywords
- include: fractions
- include: numerals
- include: constants
- include: builtins
- include: modules
- include: fractions
- include: numerals
- include: operators
- include: identifiers
@@ -70,19 +70,19 @@ contexts:
scope: constant.language.centvrion
builtins:
- match: '\b(ADIVNGE|AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DIC|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|LEGE|LONGITVDO|ORDINA|PETE|QVAERE|SCRIBE|SEMEN|SENATVS|SVBSTITVE)\b'
- match: '\b(ADDE|ADIVNGE|AVDI_NVMERVS|AVDI|AVSCVLTA|CLAVES|CONFLA|CRIBRA|DECIMATIO|DIC|DORMI|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|IASON_LEGE|IASON_SCRIBE|INSERE|IVNGE|LEGE|LITTERA|LONGITVDO|MAIVSCVLA|MINVSCVLA|MVTA|NECTE|NVMERVS|ORDINA|PETE|PETITVR|QVAERE|SCINDE|SCRIBE|SEMEN|SENATVS|SVBSTITVE|TOLLE|TYPVS)\b'
scope: support.function.builtin.centvrion
modules:
- match: '\b(FORS|FRACTIO|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\b'
- match: '\b(FORS|FRACTIO|IASON|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\b'
scope: support.class.module.centvrion
keywords:
- match: '\b(AETERNVM|ALIVD|AVGE|AVT|CAPE|CONTINVA|DEFINI|DESIGNA|DISPAR|DONICVM|DVM|ERVMPE|EST|ET|FAC|FVNCTIO|INVOCA|IN|MINVE|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT|CVM)\b'
- match: '\b(HAVD_PLVS|HAVD_MINVS|AETERNVM|ALIVD|AVGE|AVT|CAPE|CONTINVA|DEFINI|DESIGNA|DISPAR|DIVIDE|DONICVM|DVM|ERVMPE|EST|ET|FAC|FVNCTIO|GRADV|INVOCA|IN|MINVE|MINVS|MVLTIPLICA|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT|CVM)\b'
scope: keyword.control.centvrion
operators:
- match: '[+\-*/&]'
- match: '[+\-*/&@]'
scope: keyword.operator.centvrion
identifiers:

View File

@@ -0,0 +1,5 @@
TEMPTA {
DESIGNA x VT I / NVLLVS
} CAPE error {
DIC(error)
}

BIN
snippets/tempta_cape.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

3028
tests.py

File diff suppressed because it is too large Load Diff

450
tests/01_test_core____.py Normal file
View File

@@ -0,0 +1,450 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Output ---
output_tests = [
("DIC(\"hello\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello")]))]), ValStr("hello"), "hello\n"),
("DIC(\"world\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("world")]))]), ValStr("world"), "world\n"),
("DIC(III)", Program([], [ExpressionStatement(BuiltIn("DIC", [Numeral("III")]))]), ValStr("III"), "III\n"),
("DIC(X)", Program([], [ExpressionStatement(BuiltIn("DIC", [Numeral("X")]))]), ValStr("X"), "X\n"),
("DIC(MMXXV)", Program([], [ExpressionStatement(BuiltIn("DIC", [Numeral("MMXXV")]))]), ValStr("MMXXV"), "MMXXV\n"),
("DIC('hello')", Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello")]))]), ValStr("hello"), "hello\n"),
("DIC('world')", Program([], [ExpressionStatement(BuiltIn("DIC", [String("world")]))]), ValStr("world"), "world\n"),
("DIC(\"a\", \"b\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("a"), String("b")]))]), ValStr("a b"), "a b\n"),
("DIC(\"line one\")\nDIC(\"line two\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("line one")])), ExpressionStatement(BuiltIn("DIC", [String("line two")]))]), ValStr("line two"), "line one\nline two\n"),
("DIC(DIC(II))", Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("DIC", [Numeral("II")])]))]), ValStr("II"), "II\nII\n"),
("EVERRE()", Program([], [ExpressionStatement(BuiltIn("EVERRE", []))]), ValNul(), "\033[2J\033[H"),
]
class TestOutput(unittest.TestCase):
@parameterized.expand(output_tests)
def test_output(self, source, nodes, value, output):
run_test(self, source, nodes, value, output)
# --- Arithmetic ---
arithmetic_tests = [
("I + I", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"))]), ValInt(2)),
("X - III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "SYMBOL_MINUS"))]), ValInt(7)),
("III * IV", Program([], [ExpressionStatement(BinOp(Numeral("III"), Numeral("IV"), "SYMBOL_TIMES"))]), ValInt(12)),
("X / II", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"))]), ValInt(5)),
("X / III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "SYMBOL_DIVIDE"))]), ValInt(3)), # integer division: 10 // 3 = 3
("X RELIQVVM III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "KEYWORD_RELIQVVM"))]), ValInt(1)), # 10 % 3 = 1
("IX RELIQVVM III", Program([], [ExpressionStatement(BinOp(Numeral("IX"), Numeral("III"), "KEYWORD_RELIQVVM"))]), ValInt(0)), # exact divisor
("VII RELIQVVM X", Program([], [ExpressionStatement(BinOp(Numeral("VII"), Numeral("X"), "KEYWORD_RELIQVVM"))]), ValInt(7)), # dividend < divisor
("II + III * IV", Program([], [ExpressionStatement(BinOp(Numeral("II"), BinOp(Numeral("III"), Numeral("IV"), "SYMBOL_TIMES"), "SYMBOL_PLUS"))]), ValInt(14)), # precedence: 2 + (3*4) = 14
("(II + III) * IV", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("IV"), "SYMBOL_TIMES"))]), ValInt(20)), # parens: (2+3)*4 = 20
("CVM SVBNVLLA\n- III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(Numeral("III")))]), ValInt(-3)), # unary negation
("CVM SVBNVLLA\n- (II + III)", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS")))]), ValInt(-5)), # unary negation of expression
("CVM SVBNVLLA\n- - II", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(UnaryMinus(Numeral("II"))))]), ValInt(2)), # double negation
("CVM SVBNVLLA\nIII + - II", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(Numeral("III"), UnaryMinus(Numeral("II")), "SYMBOL_PLUS"))]), ValInt(1)), # unary in binary context
]
class TestArithmetic(unittest.TestCase):
@parameterized.expand(arithmetic_tests)
def test_arithmetic(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Precedence and associativity ---
#
# Precedence (lowest → highest):
# AVT < ET < (EST, DISPAR, PLVS, MINVS) < (+ -) < (* / RELIQVVM) < UMINUS < INDEX
precedence_tests = [
# * binds tighter than -: 10 - (2*3) = 4, not (10-2)*3 = 24
("X - II * III",
Program([], [ExpressionStatement(BinOp(Numeral("X"), BinOp(Numeral("II"), Numeral("III"), "SYMBOL_TIMES"), "SYMBOL_MINUS"))]),
ValInt(4)),
# / binds tighter than +: (10/2) + 3 = 8, not 10/(2+3) = 2
("X / II + III",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"), Numeral("III"), "SYMBOL_PLUS"))]),
ValInt(8)),
# + binds tighter than EST: (2+3)==5 = True, not 2+(3==5) = type error
("II + III EST V",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("V"), "KEYWORD_EST"))]),
ValBool(True)),
# * binds tighter than PLVS: (2*3)>4 = True
("II * III PLVS IV",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_TIMES"), Numeral("IV"), "KEYWORD_PLVS"))]),
ValBool(True)),
# comparison binds tighter than ET: (1==2) AND (2==2) = False AND True = False
("I EST II ET II EST II",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_EST"), "KEYWORD_ET"))]),
ValBool(False)),
# + binds tighter than DISPAR: (2+3)!=5 = False, not 2+(3!=5) = type error
("II + III DISPAR V",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("V"), "KEYWORD_DISPAR"))]),
ValBool(False)),
# DISPAR binds tighter than ET: (1!=2) AND (2!=2) = True AND False = False
("I DISPAR II ET II DISPAR II",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_DISPAR"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_DISPAR"), "KEYWORD_ET"))]),
ValBool(False)),
# ET binds tighter than AVT: True OR (False AND False) = True
("VERITAS AVT FALSITAS ET FALSITAS",
Program([], [ExpressionStatement(BinOp(Bool(True), BinOp(Bool(False), Bool(False), "KEYWORD_ET"), "KEYWORD_AVT"))]),
ValBool(True)),
# UMINUS binds tighter than *: (-2)*3 = -6, not -(2*3) = -6 (same value, different tree)
("CVM SVBNVLLA\n- II * III",
Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("II")), Numeral("III"), "SYMBOL_TIMES"))]),
ValInt(-6)),
# UMINUS binds tighter than +: (-2)+3 = 1, not -(2+3) = -5
("CVM SVBNVLLA\n- II + III",
Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("II")), Numeral("III"), "SYMBOL_PLUS"))]),
ValInt(1)),
# INDEX binds tighter than UMINUS: -(arr[I]) = -1
("CVM SVBNVLLA\n- [I, II, III][I]",
Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("I"))))]),
ValInt(-1)),
# INDEX binds tighter than NON: NON (arr[I]) = NON VERITAS = False
("NON [VERITAS, FALSITAS][I]",
Program([], [ExpressionStatement(UnaryNot(ArrayIndex(DataArray([Bool(True), Bool(False)]), Numeral("I"))))]),
ValBool(False)),
# INDEX binds tighter than +: (arr[II]) + X = 2 + 10 = 12
("[I, II, III][II] + X",
Program([], [ExpressionStatement(BinOp(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("II")), Numeral("X"), "SYMBOL_PLUS"))]),
ValInt(12)),
# left-associativity of -: (10-3)-2 = 5, not 10-(3-2) = 9
("X - III - II",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("X"), Numeral("III"), "SYMBOL_MINUS"), Numeral("II"), "SYMBOL_MINUS"))]),
ValInt(5)),
# left-associativity of /: (12/2)/3 = 2, not 12/(2/3) = 18
("XII / II / III",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("XII"), Numeral("II"), "SYMBOL_DIVIDE"), Numeral("III"), "SYMBOL_DIVIDE"))]),
ValInt(2)),
# RELIQVVM same precedence as *, /; left-associative: (17 % 5) % 2 = 0
("XVII RELIQVVM V RELIQVVM II",
Program([], [ExpressionStatement(
BinOp(BinOp(Numeral("XVII"), Numeral("V"), "KEYWORD_RELIQVVM"),
Numeral("II"), "KEYWORD_RELIQVVM"))]),
ValInt(0)),
# RELIQVVM binds tighter than +: 2 + (7 % 3) = 3, not (2+7) % 3 = 0
("II + VII RELIQVVM III",
Program([], [ExpressionStatement(
BinOp(Numeral("II"),
BinOp(Numeral("VII"), Numeral("III"), "KEYWORD_RELIQVVM"),
"SYMBOL_PLUS"))]),
ValInt(3)),
# left-associativity of AVT: (False OR True) OR False = True
("FALSITAS AVT VERITAS AVT FALSITAS",
Program([], [ExpressionStatement(BinOp(BinOp(Bool(False), Bool(True), "KEYWORD_AVT"), Bool(False), "KEYWORD_AVT"))]),
ValBool(True)),
]
class TestPrecedence(unittest.TestCase):
@parameterized.expand(precedence_tests)
def test_precedence(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Assignment ---
assignment_tests = [
("DESIGNA x VT III\nx",
Program([], [Designa(ID("x"), Numeral("III")), ExpressionStatement(ID("x"))]),
ValInt(3)),
("DESIGNA msg VT \"hello\"\nmsg",
Program([], [Designa(ID("msg"), String("hello")), ExpressionStatement(ID("msg"))]),
ValStr("hello")),
("DESIGNA msg VT 'hello'\nmsg",
Program([], [Designa(ID("msg"), String("hello")), ExpressionStatement(ID("msg"))]),
ValStr("hello")),
("DESIGNA a VT V\nDESIGNA b VT X\na + b",
Program([], [Designa(ID("a"), Numeral("V")), Designa(ID("b"), Numeral("X")),
ExpressionStatement(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"))]),
ValInt(15)),
("DESIGNA x VT II\nDESIGNA x VT x + I\nx",
Program([], [Designa(ID("x"), Numeral("II")),
Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")),
ExpressionStatement(ID("x"))]),
ValInt(3)),
# Compound assignment — AVGE (+=)
("DESIGNA x VT V\nx AVGE III\nx",
Program([], [Designa(ID("x"), Numeral("V")),
Designa(ID("x"), BinOp(ID("x"), Numeral("III"), "SYMBOL_PLUS")),
ExpressionStatement(ID("x"))]),
ValInt(8)),
# Compound assignment — MINVE (-=)
("DESIGNA x VT X\nx MINVE III\nx",
Program([], [Designa(ID("x"), Numeral("X")),
Designa(ID("x"), BinOp(ID("x"), Numeral("III"), "SYMBOL_MINUS")),
ExpressionStatement(ID("x"))]),
ValInt(7)),
# AVGE with complex expression
("DESIGNA x VT I\nx AVGE II + III\nx",
Program([], [Designa(ID("x"), Numeral("I")),
Designa(ID("x"), BinOp(ID("x"), BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), "SYMBOL_PLUS")),
ExpressionStatement(ID("x"))]),
ValInt(6)),
# AVGE inside a loop (DONICVM range is inclusive: I VSQVE III = [1, 2, 3])
("DESIGNA s VT NVLLVS\nDONICVM i VT I VSQVE III FAC {\ns AVGE i\n}\ns",
Program([], [Designa(ID("s"), Nullus()),
PerStatement(DataRangeArray(Numeral("I"), Numeral("III")), ID("i"),
[Designa(ID("s"), BinOp(ID("s"), ID("i"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("s"))]),
ValInt(6)),
# Compound assignment — MVLTIPLICA (*=)
("DESIGNA x VT III\nx MVLTIPLICA II\nx",
Program([], [Designa(ID("x"), Numeral("III")),
Designa(ID("x"), BinOp(ID("x"), Numeral("II"), "SYMBOL_TIMES")),
ExpressionStatement(ID("x"))]),
ValInt(6)),
# Compound assignment — DIVIDE (/=)
("DESIGNA x VT XII\nx DIVIDE III\nx",
Program([], [Designa(ID("x"), Numeral("XII")),
Designa(ID("x"), BinOp(ID("x"), Numeral("III"), "SYMBOL_DIVIDE")),
ExpressionStatement(ID("x"))]),
ValInt(4)),
# MVLTIPLICA with complex RHS — whole expression is captured before the op
("DESIGNA x VT II\nx MVLTIPLICA II + I\nx",
Program([], [Designa(ID("x"), Numeral("II")),
Designa(ID("x"), BinOp(ID("x"), BinOp(Numeral("II"), Numeral("I"), "SYMBOL_PLUS"), "SYMBOL_TIMES")),
ExpressionStatement(ID("x"))]),
ValInt(6)),
# DIVIDE with complex RHS
("DESIGNA x VT XX\nx DIVIDE II + II\nx",
Program([], [Designa(ID("x"), Numeral("XX")),
Designa(ID("x"), BinOp(ID("x"), BinOp(Numeral("II"), Numeral("II"), "SYMBOL_PLUS"), "SYMBOL_DIVIDE")),
ExpressionStatement(ID("x"))]),
ValInt(5)),
]
class TestAssignment(unittest.TestCase):
@parameterized.expand(assignment_tests)
def test_assignment(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Destructuring ---
destructuring_tests = [
# basic: unpack multi-return function
(
"DEFINI pair (a, b) VT { REDI (a, b) }\nDESIGNA x, y VT INVOCA pair (III, VII)\nx + y",
Program([], [
Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]),
DesignaDestructure([ID("x"), ID("y")], Invoca(ID("pair"), [Numeral("III"), Numeral("VII")])),
ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS")),
]),
ValInt(10),
),
# unpack array literal
(
"DESIGNA a, b VT [I, II]\na + b",
Program([], [
DesignaDestructure([ID("a"), ID("b")], DataArray([Numeral("I"), Numeral("II")])),
ExpressionStatement(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")),
]),
ValInt(3),
),
# three variables
(
"DESIGNA a, b, c VT [X, XX, XXX]\na + b + c",
Program([], [
DesignaDestructure([ID("a"), ID("b"), ID("c")], DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")])),
ExpressionStatement(BinOp(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ID("c"), "SYMBOL_PLUS")),
]),
ValInt(60),
),
# destructure into individual use
(
"DEFINI pair (a, b) VT { REDI (a, b) }\nDESIGNA x, y VT INVOCA pair (V, II)\nDIC(x)\nDIC(y)",
Program([], [
Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]),
DesignaDestructure([ID("x"), ID("y")], Invoca(ID("pair"), [Numeral("V"), Numeral("II")])),
ExpressionStatement(BuiltIn("DIC", [ID("x")])),
ExpressionStatement(BuiltIn("DIC", [ID("y")])),
]),
ValStr("II"),
"V\nII\n",
),
]
class TestDestructuring(unittest.TestCase):
@parameterized.expand(destructuring_tests)
def test_destructuring(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- Functions ---
function_tests = [
(
"DEFINI bis (n) VT { REDI (n * II) }\nINVOCA bis (III)",
Program([], [
Defini(ID("bis"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]),
ExpressionStatement(Invoca(ID("bis"), [Numeral("III")])),
]),
ValInt(6),
),
(
"DEFINI add (a, b) VT { REDI (a + b) }\nINVOCA add (III, IV)",
Program([], [
Defini(ID("add"), [ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])]),
ExpressionStatement(Invoca(ID("add"), [Numeral("III"), Numeral("IV")])),
]),
ValInt(7),
),
# Fibonacci: fib(n<3)=1, fib(n)=fib(n-1)+fib(n-2)
(
"DEFINI fib (n) VT {\nSI n MINVS III TVNC { REDI (I) } ALIVD { REDI (INVOCA fib (n - I) + INVOCA fib (n - II)) }\n}\nINVOCA fib (VII)",
Program([], [
Defini(ID("fib"), [ID("n")], [
SiStatement(
BinOp(ID("n"), Numeral("III"), "KEYWORD_MINVS"),
[Redi([Numeral("I")])],
[Redi([BinOp(
Invoca(ID("fib"), [BinOp(ID("n"), Numeral("I"), "SYMBOL_MINUS")]),
Invoca(ID("fib"), [BinOp(ID("n"), Numeral("II"), "SYMBOL_MINUS")]),
"SYMBOL_PLUS",
)])],
),
]),
ExpressionStatement(Invoca(ID("fib"), [Numeral("VII")])),
]),
ValInt(13),
),
]
class TestFunctions(unittest.TestCase):
@parameterized.expand(function_tests)
def test_functions(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Function edge cases ---
function_edge_tests = [
# no explicit REDI → returns ValNul
("DEFINI f () VT { I }\nINVOCA f ()",
Program([], [Defini(ID("f"), [], [ExpressionStatement(Numeral("I"))]), ExpressionStatement(Invoca(ID("f"), []))]),
ValNul()),
# REDI multiple values → ValList
(
"DEFINI pair (a, b) VT { REDI (a, b) }\nINVOCA pair (I, II)",
Program([], [
Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]),
ExpressionStatement(Invoca(ID("pair"), [Numeral("I"), Numeral("II")])),
]),
ValList([ValInt(1), ValInt(2)]),
),
# function doesn't mutate outer vtable
(
"DESIGNA x VT I\nDEFINI f () VT { DESIGNA x VT V\nREDI (x) }\nINVOCA f ()\nx",
Program([], [
Designa(ID("x"), Numeral("I")),
Defini(ID("f"), [], [Designa(ID("x"), Numeral("V")), Redi([ID("x")])]),
ExpressionStatement(Invoca(ID("f"), [])),
ExpressionStatement(ID("x")),
]),
ValInt(1),
),
# function can read outer vtable (closure-like)
(
"DESIGNA x VT VII\nDEFINI f () VT { REDI (x) }\nINVOCA f ()",
Program([], [
Designa(ID("x"), Numeral("VII")),
Defini(ID("f"), [], [Redi([ID("x")])]),
ExpressionStatement(Invoca(ID("f"), [])),
]),
ValInt(7),
),
# parameter shadows outer variable inside function
(
"DESIGNA n VT I\nDEFINI f (n) VT { REDI (n * II) }\nINVOCA f (X)\nn",
Program([], [
Designa(ID("n"), Numeral("I")),
Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]),
ExpressionStatement(Invoca(ID("f"), [Numeral("X")])),
ExpressionStatement(ID("n")),
]),
ValInt(1),
),
# function aliasing: assign f to g, invoke via g
(
"DEFINI f (n) VT { REDI (n * II) }\nDESIGNA g VT f\nINVOCA g (V)",
Program([], [
Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]),
Designa(ID("g"), ID("f")),
ExpressionStatement(Invoca(ID("g"), [Numeral("V")])),
]),
ValInt(10),
),
# alias is independent: redefining f doesn't affect g
(
"DEFINI f (n) VT { REDI (n * II) }\nDESIGNA g VT f\nDEFINI f (n) VT { REDI (n * III) }\nINVOCA g (V)",
Program([], [
Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]),
Designa(ID("g"), ID("f")),
Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("III"), "SYMBOL_TIMES")])]),
ExpressionStatement(Invoca(ID("g"), [Numeral("V")])),
]),
ValInt(10),
),
# REDI inside SI exits function, skips remaining statements in block
(
"DEFINI f () VT {\nSI VERITAS TVNC {\nREDI (I)\nREDI (II)\n}\n}\nINVOCA f ()",
Program([],[
Defini(ID("f"), [], [SiStatement(Bool(True),[Redi([Numeral("I")]),Redi([Numeral("II")])],None)]),
ExpressionStatement(Invoca(ID("f"),[]))
]),
ValInt(1),
),
# REDI inside DVM exits loop and function
(
"DEFINI f () VT {\nDESIGNA x VT I\nDVM FALSITAS FAC {\nREDI (x)\n}\n}\nINVOCA f ()",
Program([],[
Defini(ID("f"), [], [
Designa(ID("x"), Numeral("I")),
DumStatement(Bool(False), [Redi([ID("x")])])
]),
ExpressionStatement(Invoca(ID("f"),[]))
]),
ValInt(1),
),
# REDI inside PER exits loop and function
(
"DEFINI f () VT {\nPER x IN [I, II, III] FAC {\nSI x EST II TVNC {\nREDI (x)\n}\n}\n}\nINVOCA f ()",
Program([],[
Defini(ID("f"), [], [
PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("x"), [
SiStatement(BinOp(ID("x"), Numeral("II"), "KEYWORD_EST"), [
Redi([ID("x")])
], None)
])
]),
ExpressionStatement(Invoca(ID("f"),[]))
]),
ValInt(2),
),
# REDI inside nested loops exits all loops and function
(
"DEFINI f () VT {\nDESIGNA x VT I\nDVM FALSITAS FAC {\nDVM FALSITAS FAC {\nREDI (x)\n}\n}\n}\nINVOCA f ()",
Program([],[
Defini(ID("f"), [], [
Designa(ID("x"), Numeral("I")),
DumStatement(Bool(False), [
DumStatement(Bool(False), [
Redi([ID("x")])
])
])
]),
ExpressionStatement(Invoca(ID("f"),[]))
]),
ValInt(1),
),
]
class TestFunctionEdge(unittest.TestCase):
@parameterized.expand(function_edge_tests)
def test_function_edge(self, source, nodes, value):
run_test(self, source, nodes, value)

418
tests/02_test_con_flow.py Normal file
View File

@@ -0,0 +1,418 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Control flow ---
control_tests = [
# SI without ALIVD — true branch
("SI VERITAS TVNC { DESIGNA r VT I }\nr",
Program([], [SiStatement(Bool(True), [Designa(ID("r"), Numeral("I"))], None), ExpressionStatement(ID("r"))]),
ValInt(1)),
# SI without ALIVD — false branch
("SI FALSITAS TVNC { DESIGNA r VT I }",
Program([], [SiStatement(Bool(False), [Designa(ID("r"), Numeral("I"))], None)]),
ValNul()),
# SI with ALIVD — true branch
("SI VERITAS TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(Bool(True), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(1)),
# SI with ALIVD — false branch
("SI FALSITAS TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(Bool(False), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(2)),
# SI with comparison — equal
("SI I EST I TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(1)),
# SI with comparison — unequal
("SI I EST II TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(2)),
# SI MINVS
("SI I MINVS II TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_MINVS"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(1)),
# SI PLVS
("SI II PLVS I TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_PLVS"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(1)),
# ALIVD SI chain
(
"SI I EST II TVNC { DESIGNA r VT I } ALIVD SI I EST I TVNC { DESIGNA r VT II } ALIVD { DESIGNA r VT III }\nr",
Program([], [
SiStatement(
BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"),
[Designa(ID("r"), Numeral("I"))],
[SiStatement(
BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"),
[Designa(ID("r"), Numeral("II"))],
[Designa(ID("r"), Numeral("III"))],
)],
),
ExpressionStatement(ID("r")),
]),
ValInt(2),
),
# DVM (while not): loops until condition is true
(
"DESIGNA x VT I\nDVM x EST III FAC {\nDESIGNA x VT x + I\n}\nx",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_EST"), [Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("x")),
]),
ValInt(3),
),
# DVM with ERVMPE — loop body prints (testing DIC + ERVMPE together)
("DESIGNA x VT I\nDVM FALSITAS FAC {\nDIC(x)\nERVMPE\n}",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(Bool(False), [ExpressionStatement(BuiltIn("DIC", [ID("x")])), Erumpe()]),
]),
ValStr("I"), "I\n"),
# AETERNVM is sugar for DVM FALSITAS — must produce the same AST.
("DESIGNA x VT I\nAETERNVM FAC {\nDIC(x)\nERVMPE\n}",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(Bool(False), [ExpressionStatement(BuiltIn("DIC", [ID("x")])), Erumpe()]),
]),
ValStr("I"), "I\n"),
# AETERNVM with counter + ERVMPE on condition
("DESIGNA x VT I\nAETERNVM FAC {\nSI x EST III TVNC { ERVMPE }\nDESIGNA x VT x + I\n}\nx",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(Bool(False), [
SiStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None),
Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")),
]),
ExpressionStatement(ID("x")),
]),
ValInt(3)),
# AETERNVM with CONTINVA — skip printing III; ERVMPE after V.
# Return value is ValNul because the iteration that triggers ERVMPE runs
# Designa first (resetting last_val); we test on output, which is the point.
("DESIGNA x VT NVLLVS\nAETERNVM FAC {\nDESIGNA x VT x + I\nSI x PLVS V TVNC { ERVMPE }\nSI x EST III TVNC { CONTINVA }\nDIC(x)\n}",
Program([], [
Designa(ID("x"), Nullus()),
DumStatement(Bool(False), [
Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")),
SiStatement(BinOp(ID("x"), Numeral("V"), "KEYWORD_PLVS"), [Erumpe()], None),
SiStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_EST"), [Continva()], None),
ExpressionStatement(BuiltIn("DIC", [ID("x")])),
]),
]),
ValNul(), "I\nII\nIV\nV\n"),
# REDI inside AETERNVM (inside DEFINI) — exits both loop and function
(
"DEFINI f () VT {\nDESIGNA x VT I\nAETERNVM FAC {\nREDI (x)\n}\n}\nINVOCA f ()",
Program([], [
Defini(ID("f"), [], [
Designa(ID("x"), Numeral("I")),
DumStatement(Bool(False), [Redi([ID("x")])]),
]),
ExpressionStatement(Invoca(ID("f"), [])),
]),
ValInt(1),
),
# PER foreach
("PER i IN [I, II, III] FAC { DIC(i) }",
Program([], [PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("III"), "I\nII\nIII\n"),
# DONICVM range loop
("DONICVM i VT I VSQVE V FAC { DIC(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("V")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("V"), "I\nII\nIII\nIV\nV\n"),
# PER destructuring
("PER a, b IN [[I, II], [III, IV]] FAC { DIC(a + b) }",
Program([], [PerStatement(
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]),
[ID("a"), ID("b")],
[ExpressionStatement(BuiltIn("DIC", [BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")]))])]),
ValStr("VII"), "III\nVII\n"),
# PER destructuring: three variables
("PER a, b, c IN [[I, II, III]] FAC { DIC(a + b + c) }",
Program([], [PerStatement(
DataArray([DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]),
[ID("a"), ID("b"), ID("c")],
[ExpressionStatement(BuiltIn("DIC", [BinOp(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ID("c"), "SYMBOL_PLUS")]))])]),
ValStr("VI"), "VI\n"),
]
class TestControl(unittest.TestCase):
@parameterized.expand(control_tests)
def test_control(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- Loop edge cases ---
loop_edge_tests = [
# [III VSQVE III] = [3] — single iteration
("DONICVM i VT III VSQVE III FAC { DIC(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("III"), Numeral("III")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("III"), "III\n"),
# empty array — body never runs
("PER i IN [] FAC { DIC(i) }",
Program([], [PerStatement(DataArray([]), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValNul(), ""),
# PER breaks on element 2 — last assigned i is 2
("PER i IN [I, II, III] FAC { SI i EST II TVNC { ERVMPE } }\ni",
Program([], [
PerStatement(
DataArray([Numeral("I"), Numeral("II"), Numeral("III")]),
ID("i"),
[SiStatement(BinOp(ID("i"), Numeral("II"), "KEYWORD_EST"), [Erumpe()], None)],
),
ExpressionStatement(ID("i")),
]),
ValInt(2), ""),
# nested DVM: inner always breaks; outer runs until btr==3
("DESIGNA btr VT I\nDVM btr EST III FAC {\nDVM FALSITAS FAC {\nERVMPE\n}\nDESIGNA btr VT btr + I\n}\nbtr",
Program([], [
Designa(ID("btr"), Numeral("I")),
DumStatement(
BinOp(ID("btr"), Numeral("III"), "KEYWORD_EST"),
[DumStatement(Bool(False), [Erumpe()]), Designa(ID("btr"), BinOp(ID("btr"), Numeral("I"), "SYMBOL_PLUS"))],
),
ExpressionStatement(ID("btr")),
]),
ValInt(3), ""),
# nested PER: inner always breaks on first element; outer completes both iterations
# cnt starts at 1, increments twice → 3
("DESIGNA cnt VT I\nPER i IN [I, II] FAC {\nPER k IN [I, II] FAC {\nERVMPE\n}\nDESIGNA cnt VT cnt + I\n}\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
PerStatement(
DataArray([Numeral("I"), Numeral("II")]),
ID("i"),
[PerStatement(DataArray([Numeral("I"), Numeral("II")]), ID("k"), [Erumpe()]),
Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))],
),
ExpressionStatement(ID("cnt")),
]),
ValInt(3), ""),
# PER with CONTINVA: skip odd numbers, sum evens
# [I,II,III,IV] → skip I and III; cnt increments on II and IV → cnt = III
("DESIGNA cnt VT I\nPER i IN [I, II, III, IV] FAC {\nSI i EST I AVT i EST III TVNC { CONTINVA }\nDESIGNA cnt VT cnt + I\n}\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
PerStatement(
DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV")]),
ID("i"),
[SiStatement(BinOp(BinOp(ID("i"), Numeral("I"), "KEYWORD_EST"), BinOp(ID("i"), Numeral("III"), "KEYWORD_EST"), "KEYWORD_AVT"), [Continva()], None),
Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))],
),
ExpressionStatement(ID("cnt")),
]),
ValInt(3), ""),
# DVM with CONTINVA: skip body when x is II, increment regardless
# x goes 1→2→3; on x=2 we continue (no DIC); DIC fires for x=1 and x=3
("DESIGNA x VT I\nDVM x EST IV FAC {\nSI x EST II TVNC { DESIGNA x VT x + I\nCONTINVA }\nDIC(x)\nDESIGNA x VT x + I\n}\nx",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(
BinOp(ID("x"), Numeral("IV"), "KEYWORD_EST"),
[SiStatement(BinOp(ID("x"), Numeral("II"), "KEYWORD_EST"),
[Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")), Continva()], None),
ExpressionStatement(BuiltIn("DIC", [ID("x")])),
Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))],
),
ExpressionStatement(ID("x")),
]),
ValInt(4), "I\nIII\n"),
# nested PER: CONTINVA in inner only skips rest of inner body; outer still increments
("DESIGNA cnt VT I\nPER i IN [I, II] FAC {\nPER k IN [I, II] FAC {\nCONTINVA\nDESIGNA cnt VT cnt + I\n}\nDESIGNA cnt VT cnt + I\n}\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
PerStatement(
DataArray([Numeral("I"), Numeral("II")]),
ID("i"),
[PerStatement(DataArray([Numeral("I"), Numeral("II")]), ID("k"),
[Continva(), Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))]),
Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))],
),
ExpressionStatement(ID("cnt")),
]),
ValInt(3), ""),
# DONICVM with CONTINVA: skip value III, count remaining (I VSQVE IV = [1,2,3,4], skip 3 → 3 increments)
("DESIGNA cnt VT I\nDONICVM i VT I VSQVE IV FAC {\nSI i EST III TVNC { CONTINVA }\nDESIGNA cnt VT cnt + I\n}\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
PerStatement(
DataRangeArray(Numeral("I"), Numeral("IV")),
ID("i"),
[SiStatement(BinOp(ID("i"), Numeral("III"), "KEYWORD_EST"), [Continva()], None),
Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))],
),
ExpressionStatement(ID("cnt")),
]),
ValInt(4)),
# DVM condition true from start — body never runs
("DESIGNA x VT I\nDVM VERITAS FAC {\nDESIGNA x VT x + I\n}\nx",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(Bool(True), [Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("x")),
]),
ValInt(1), ""),
# two iterations: [I VSQVE II] = [1, 2]
("DONICVM i VT I VSQVE II FAC { DIC(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("II")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("II"), "I\nII\n"),
# single iteration: [I VSQVE I] = [1]
("DONICVM i VT I VSQVE I FAC { DIC(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("I")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("I"), "I\n"),
# empty range: [V VSQVE I] = []
("DESIGNA x VT NVLLVS\nDONICVM i VT V VSQVE I FAC { DESIGNA x VT x + i }\nx",
Program([], [Designa(ID("x"), Nullus()),
PerStatement(DataRangeArray(Numeral("V"), Numeral("I")), ID("i"),
[Designa(ID("x"), BinOp(ID("x"), ID("i"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("x"))]),
ValNul(), ""),
# PER destructuring with ERVMPE
("DESIGNA r VT I\nPER a, b IN [[I, II], [III, IV], [V, VI]] FAC {\nSI a EST III TVNC { ERVMPE }\nDESIGNA r VT r + a + b\n}\nr",
Program([], [
Designa(ID("r"), Numeral("I")),
PerStatement(
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")]), DataArray([Numeral("V"), Numeral("VI")])]),
[ID("a"), ID("b")],
[SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None),
Designa(ID("r"), BinOp(BinOp(ID("r"), ID("a"), "SYMBOL_PLUS"), ID("b"), "SYMBOL_PLUS"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(4)), # 1 + 1 + 2 = 4, breaks before [III, IV]
# PER destructuring with REDI
("DEFINI f () VT {\nPER a, b IN [[I, II], [III, IV]] FAC {\nSI a EST III TVNC { REDI (b) }\n}\n}\nINVOCA f ()",
Program([], [
Defini(ID("f"), [],
[PerStatement(
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]),
[ID("a"), ID("b")],
[SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Redi([ID("b")])], None)],
)]),
ExpressionStatement(Invoca(ID("f"), [])),
]),
ValInt(4)), # returns b=IV when a=III
# DONICVM GRADV II, endpoint hit exactly: [I, III, V]
("DONICVM i VT I VSQVE V GRADV II FAC { DIC(i) }",
Program([], [PerStatement(
DataRangeArray(Numeral("I"), Numeral("V"), Numeral("II")),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("V"), "I\nIII\nV\n"),
# DONICVM GRADV II, endpoint overshot: VI excluded, stops at V
("DONICVM i VT I VSQVE VI GRADV II FAC { DIC(i) }",
Program([], [PerStatement(
DataRangeArray(Numeral("I"), Numeral("VI"), Numeral("II")),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("V"), "I\nIII\nV\n"),
# DONICVM GRADV I is equivalent to default step
("DONICVM i VT I VSQVE III GRADV I FAC { DIC(i) }",
Program([], [PerStatement(
DataRangeArray(Numeral("I"), Numeral("III"), Numeral("I")),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("III"), "I\nII\nIII\n"),
# DONICVM descending by I
("CVM SVBNVLLA\nDONICVM i VT V VSQVE I GRADV - I FAC { DIC(i) }",
Program([ModuleCall("SVBNVLLA")], [PerStatement(
DataRangeArray(Numeral("V"), Numeral("I"), UnaryMinus(Numeral("I"))),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("I"), "V\nIV\nIII\nII\nI\n"),
# DONICVM descending by II, endpoint overshot
("CVM SVBNVLLA\nDONICVM i VT X VSQVE I GRADV - II FAC { DIC(i) }",
Program([ModuleCall("SVBNVLLA")], [PerStatement(
DataRangeArray(Numeral("X"), Numeral("I"), UnaryMinus(Numeral("II"))),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("II"), "X\nVIII\nVI\nIV\nII\n"),
# DONICVM with step bound to a variable
("DESIGNA s VT II\nDONICVM i VT I VSQVE V GRADV s FAC { DIC(i) }",
Program([], [
Designa(ID("s"), Numeral("II")),
PerStatement(
DataRangeArray(Numeral("I"), Numeral("V"), ID("s")),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("V"), "I\nIII\nV\n"),
# DONICVM from == to with nonzero step: single iteration
("DONICVM i VT III VSQVE III GRADV II FAC { DIC(i) }",
Program([], [PerStatement(
DataRangeArray(Numeral("III"), Numeral("III"), Numeral("II")),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("III"), "III\n"),
# DONICVM direction mismatch — negative step with ascending bounds: body never runs
("CVM SVBNVLLA\nDESIGNA x VT NVLLVS\nDONICVM i VT I VSQVE X GRADV - I FAC { DESIGNA x VT x + i }\nx",
Program([ModuleCall("SVBNVLLA")], [
Designa(ID("x"), Nullus()),
PerStatement(
DataRangeArray(Numeral("I"), Numeral("X"), UnaryMinus(Numeral("I"))),
ID("i"),
[Designa(ID("x"), BinOp(ID("x"), ID("i"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("x"))]),
ValNul(), ""),
# Range-array literal with GRADV used via PER
("PER i IN [I VSQVE V GRADV II] FAC { DIC(i) }",
Program([], [PerStatement(
DataRangeArray(Numeral("I"), Numeral("V"), Numeral("II")),
ID("i"),
[ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("V"), "I\nIII\nV\n"),
# DONICVM GRADV II with CONTINVA: skip value V, print the others
("DONICVM i VT I VSQVE IX GRADV II FAC {\nSI i EST V TVNC { CONTINVA }\nDIC(i)\n}",
Program([], [PerStatement(
DataRangeArray(Numeral("I"), Numeral("IX"), Numeral("II")),
ID("i"),
[SiStatement(BinOp(ID("i"), Numeral("V"), "KEYWORD_EST"), [Continva()], None),
ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("IX"), "I\nIII\nVII\nIX\n"),
# DONICVM GRADV II with ERVMPE: stop at V (last successful DIC was III)
("DONICVM i VT I VSQVE IX GRADV II FAC {\nSI i EST V TVNC { ERVMPE }\nDIC(i)\n}",
Program([], [PerStatement(
DataRangeArray(Numeral("I"), Numeral("IX"), Numeral("II")),
ID("i"),
[SiStatement(BinOp(ID("i"), Numeral("V"), "KEYWORD_EST"), [Erumpe()], None),
ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]),
ValStr("III"), "I\nIII\n"),
]
class TestLoopEdge(unittest.TestCase):
@parameterized.expand(loop_edge_tests)
def test_loop_edge(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- SI/DVM: boolean condition enforcement ---
dvm_bool_condition_tests = [
# DVM exits when condition becomes true (boolean comparison)
(
"DESIGNA x VT I\nDVM x PLVS III FAC {\nDESIGNA x VT x + I\n}\nx",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_PLVS"), [Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("x")),
]),
ValInt(4),
),
]
class TestDvmBoolCondition(unittest.TestCase):
@parameterized.expand(dvm_bool_condition_tests)
def test_dvm_bool_condition(self, source, nodes, value):
run_test(self, source, nodes, value)

322
tests/03_test_builtins.py Normal file
View File

@@ -0,0 +1,322 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Builtins ---
builtin_tests = [
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(3), "", ["III"]),
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(10), "", ["X"]),
("CVM FORS\nDESIGNA a VT [I, II, III]\nDIC(a[FORTVITVS_NVMERVS(I, LONGITVDO(a))])", Program([ModuleCall("FORS")], [Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), ExpressionStatement(BuiltIn("DIC", [ArrayIndex(ID("a"), BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), BuiltIn("LONGITVDO", [ID("a")])]))]))]), ValStr("I"), "I\n"),
("AVDI()", Program([], [ExpressionStatement(BuiltIn("AVDI", []))]), ValStr("hello"), "", ["hello"]),
("LONGITVDO([I, II, III])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValInt(3)),
("LONGITVDO([])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([])]))]), ValInt(0)),
('LONGITVDO("salve")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("salve")]))]), ValInt(5)),
('LONGITVDO("")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("")]))]), ValInt(0)),
("CVM FORS\nDIC(FORTVITA_ELECTIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITA_ELECTIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("I"), "I\n"),
("CVM FORS\nSEMEN(XLII)\nDIC(FORTVITVS_NVMERVS(I, C))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("C")])]))]), ValStr("XXXIII"), "XXXIII\n"),
# DECIMATIO: seed 42, 10 elements → removes 1 (element III)
("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X")])])]))]), ValStr("[I II IV V VI VII VIII IX X]"), "[I II IV V VI VII VIII IX X]\n"),
# DECIMATIO: seed 1, 3 elements → 3//10=0, nothing removed
("CVM FORS\nSEMEN(I)\nDIC(DECIMATIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("I")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("[I II III]"), "[I II III]\n"),
# DECIMATIO: empty array → empty array
("CVM FORS\nDIC(DECIMATIO([]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([])])]))]), ValStr("[]"), "[]\n"),
# DECIMATIO: seed 42, 20 elements → removes 2 (elements XIII and XII)
("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X"), Numeral("XI"), Numeral("XII"), Numeral("XIII"), Numeral("XIV"), Numeral("XV"), Numeral("XVI"), Numeral("XVII"), Numeral("XVIII"), Numeral("XIX"), Numeral("XX")])])]))]), ValStr("[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]"), "[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]\n"),
# SENATVS: majority true → VERITAS
("SENATVS(VERITAS, VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False)]))]), ValBool(True)),
# SENATVS: majority false → FALSITAS
("SENATVS(FALSITAS, FALSITAS, VERITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(False), Bool(False), Bool(True)]))]), ValBool(False)),
# SENATVS: tie → FALSITAS
("SENATVS(VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(False)]))]), ValBool(False)),
# SENATVS: 4-arg tie → FALSITAS
("SENATVS(VERITAS, VERITAS, FALSITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False), Bool(False)]))]), ValBool(False)),
# SENATVS: single true → VERITAS
("SENATVS(VERITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True)]))]), ValBool(True)),
# SENATVS: single false → FALSITAS
("SENATVS(FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(False)]))]), ValBool(False)),
# SENATVS: empty → FALSITAS (vacuous)
("SENATVS()", Program([], [ExpressionStatement(BuiltIn("SENATVS", []))]), ValBool(False)),
# SENATVS: all true <20><> VERITAS
("SENATVS(VERITAS, VERITAS, VERITAS, VERITAS, VERITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(True), Bool(True), Bool(True)]))]), ValBool(True)),
# SENATVS: array input, majority true → VERITAS
("SENATVS([VERITAS, VERITAS, FALSITAS])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([Bool(True), Bool(True), Bool(False)])]))]), ValBool(True)),
# SENATVS: array input, majority false → FALSITAS
("SENATVS([FALSITAS, FALSITAS, VERITAS])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([Bool(False), Bool(False), Bool(True)])]))]), ValBool(False)),
# SENATVS: array input, empty → FALSITAS
("SENATVS([])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([])]))]), ValBool(False)),
# ORDINA: sort integers
("ORDINA([III, I, II])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("III"), Numeral("I"), Numeral("II")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# ORDINA: sort strings
('ORDINA(["c", "a", "b"])', Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([String("c"), String("a"), String("b")])]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])),
# ORDINA: empty list
("ORDINA([])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([])]))]), ValList([])),
# ORDINA: single element
("ORDINA([V])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("V")])]))]), ValList([ValInt(5)])),
# ORDINA: already sorted
("ORDINA([I, II, III])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# ORDINA: duplicates
("ORDINA([II, I, II])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("II"), Numeral("I"), Numeral("II")])]))]), ValList([ValInt(1), ValInt(2), ValInt(2)])),
# ORDINA: negative numbers
("CVM SVBNVLLA\nORDINA([-II, III, -I])", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([UnaryMinus(Numeral("II")), Numeral("III"), UnaryMinus(Numeral("I"))])]))]), ValList([ValInt(-2), ValInt(-1), ValInt(3)])),
# ORDINA: fractions only
("CVM FRACTIO\nORDINA([IIIS, S, IIS])", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Fractio("IIIS"), Fractio("S"), Fractio("IIS")])]))]), ValList([ValFrac(Fraction(1, 2)), ValFrac(Fraction(5, 2)), ValFrac(Fraction(7, 2))])),
# ORDINA: mixed integers and fractions
("CVM FRACTIO\nORDINA([III, S, II])", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("III"), Fractio("S"), Numeral("II")])]))]), ValList([ValFrac(Fraction(1, 2)), ValInt(2), ValInt(3)])),
# ORDINA: array passed via variable
("DESIGNA x VT [III, I, II]\nORDINA(x)", Program([], [Designa(ID("x"), DataArray([Numeral("III"), Numeral("I"), Numeral("II")])), ExpressionStatement(BuiltIn("ORDINA", [ID("x")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# ORDINA: descending sort with named comparator
("DEFINI gt (a, b) VT { REDI (a PLVS b) }\nORDINA([II, V, I, III], gt)", Program([], [Defini(ID("gt"), [ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "KEYWORD_PLVS")])]), ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("II"), Numeral("V"), Numeral("I"), Numeral("III")]), ID("gt")]))]), ValList([ValInt(5), ValInt(3), ValInt(2), ValInt(1)])),
# ORDINA: ascending sort with inline FVNCTIO comparator
("ORDINA([V, I, III], FVNCTIO (a, b) VT { REDI (a MINVS b) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("V"), Numeral("I"), Numeral("III")]), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "KEYWORD_MINVS")])])]))]), ValList([ValInt(1), ValInt(3), ValInt(5)])),
# ORDINA: empty list with comparator
("ORDINA([], FVNCTIO (a, b) VT { REDI (VERITAS) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([]), Fvnctio([ID("a"), ID("b")], [Redi([Bool(True)])])]))]), ValList([])),
# ORDINA: single element with comparator
("ORDINA([VII], FVNCTIO (a, b) VT { REDI (VERITAS) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("VII")]), Fvnctio([ID("a"), ID("b")], [Redi([Bool(True)])])]))]), ValList([ValInt(7)])),
# ORDINA: comparator sorting two-element subarrays by first element
("ORDINA([[II, I], [I, II]], FVNCTIO (a, b) VT { REDI (a[I] PLVS b[I]) })", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([DataArray([Numeral("II"), Numeral("I")]), DataArray([Numeral("I"), Numeral("II")])]), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ArrayIndex(ID("a"), Numeral("I")), ArrayIndex(ID("b"), Numeral("I")), "KEYWORD_PLVS")])])]))]), ValList([ValList([ValInt(2), ValInt(1)]), ValList([ValInt(1), ValInt(2)])])),
# MVTA: empty array → empty array
("MVTA([], FVNCTIO (x) VT { REDI (x + x) })", Program([], [ExpressionStatement(BuiltIn("MVTA", [DataArray([]), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_PLUS")])])]))]), ValList([])),
# MVTA: single element doubled
("MVTA([V], FVNCTIO (x) VT { REDI (x + x) })", Program([], [ExpressionStatement(BuiltIn("MVTA", [DataArray([Numeral("V")]), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_PLUS")])])]))]), ValList([ValInt(10)])),
# MVTA: multiple elements with named function
("DEFINI dbl (x) VT { REDI (x + x) }\nMVTA([I, II, III], dbl)", Program([], [Defini(ID("dbl"), [ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_PLUS")])]), ExpressionStatement(BuiltIn("MVTA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("dbl")]))]), ValList([ValInt(2), ValInt(4), ValInt(6)])),
# MVTA: inline FVNCTIO squaring
("MVTA([I, II, III], FVNCTIO (x) VT { REDI (x * x) })", Program([], [ExpressionStatement(BuiltIn("MVTA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_TIMES")])])]))]), ValList([ValInt(1), ValInt(4), ValInt(9)])),
# MVTA: print form to confirm output rendering
("DIC(MVTA([I, II, III], FVNCTIO (x) VT { REDI (x + x) }))", Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("MVTA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_PLUS")])])])]))]), ValStr("[II IV VI]"), "[II IV VI]\n"),
# MVTA: array passed via variable
("DESIGNA xs VT [I, II, III]\nDIC(MVTA(xs, FVNCTIO (x) VT { REDI (x + I) }))", Program([], [Designa(ID("xs"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("MVTA", [ID("xs"), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])])])]))]), ValStr("[II III IV]"), "[II III IV]\n"),
# MVTA: callback may return a different type than input
('DIC(MVTA([I, II, III], FVNCTIO (x) VT { REDI (LITTERA(x)) }))', Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("MVTA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Fvnctio([ID("x")], [Redi([BuiltIn("LITTERA", [ID("x")])])])])]))]), ValStr("[I II III]"), "[I II III]\n"),
# CRIBRA: empty array → empty array
("CRIBRA([], FVNCTIO (x) VT { REDI (VERITAS) })", Program([], [ExpressionStatement(BuiltIn("CRIBRA", [DataArray([]), Fvnctio([ID("x")], [Redi([Bool(True)])])]))]), ValList([])),
# CRIBRA: predicate always true keeps everything
("CRIBRA([I, II, III], FVNCTIO (x) VT { REDI (VERITAS) })", Program([], [ExpressionStatement(BuiltIn("CRIBRA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Fvnctio([ID("x")], [Redi([Bool(True)])])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# CRIBRA: predicate always false drops everything
("CRIBRA([I, II, III], FVNCTIO (x) VT { REDI (FALSITAS) })", Program([], [ExpressionStatement(BuiltIn("CRIBRA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Fvnctio([ID("x")], [Redi([Bool(False)])])]))]), ValList([])),
# CRIBRA: keep elements ≤ III
("CRIBRA([I, II, III, IV, V], FVNCTIO (x) VT { REDI (x HAVD_PLVS III) })", Program([], [ExpressionStatement(BuiltIn("CRIBRA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")]), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("III"), "KEYWORD_HAVD_PLVS")])])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# CRIBRA: with named predicate
("DEFINI big (x) VT { REDI (x PLVS III) }\nCRIBRA([I, II, III, IV, V], big)", Program([], [Defini(ID("big"), [ID("x")], [Redi([BinOp(ID("x"), Numeral("III"), "KEYWORD_PLVS")])]), ExpressionStatement(BuiltIn("CRIBRA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")]), ID("big")]))]), ValList([ValInt(4), ValInt(5)])),
# CRIBRA: print form
("DIC(CRIBRA([I, II, III, IV, V], FVNCTIO (x) VT { REDI (x PLVS II) }))", Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("CRIBRA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")]), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("II"), "KEYWORD_PLVS")])])])]))]), ValStr("[III IV V]"), "[III IV V]\n"),
# CONFLA: empty array returns initial unchanged
("CONFLA([], V, FVNCTIO (a, b) VT { REDI (a + b) })", Program([], [ExpressionStatement(BuiltIn("CONFLA", [DataArray([]), Numeral("V"), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])])]))]), ValInt(5)),
# CONFLA: sum from initial I (so result = 1 + 1 + 2 + 3 = 7)
("CONFLA([I, II, III], I, FVNCTIO (a, b) VT { REDI (a + b) })", Program([], [ExpressionStatement(BuiltIn("CONFLA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("I"), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])])]))]), ValInt(7)),
# CONFLA: product starting from I
("CONFLA([II, III, IV], I, FVNCTIO (a, b) VT { REDI (a * b) })", Program([], [ExpressionStatement(BuiltIn("CONFLA", [DataArray([Numeral("II"), Numeral("III"), Numeral("IV")]), Numeral("I"), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_TIMES")])])]))]), ValInt(24)),
# CONFLA: with named function
("DEFINI addi (a, b) VT { REDI (a + b) }\nCONFLA([I, II, III, IV], V, addi)", Program([], [Defini(ID("addi"), [ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])]), ExpressionStatement(BuiltIn("CONFLA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV")]), Numeral("V"), ID("addi")]))]), ValInt(15)),
# CONFLA: string concatenation
('CONFLA(["b", "c"], "a", FVNCTIO (a, b) VT { REDI (a & b) })', Program([], [ExpressionStatement(BuiltIn("CONFLA", [DataArray([String("b"), String("c")]), String("a"), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_AMPERSAND")])])]))]), ValStr("abc")),
# CONFLA: print sum
("DIC(CONFLA([I, II, III, IV], I, FVNCTIO (a, b) VT { REDI (a + b) }))", Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("CONFLA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV")]), Numeral("I"), Fvnctio([ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])])])]))]), ValStr("XI"), "XI\n"),
# ADDE: append to non-empty
("ADDE([I, II], III)", Program([], [ExpressionStatement(BuiltIn("ADDE", [DataArray([Numeral("I"), Numeral("II")]), Numeral("III")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# ADDE: append to empty
("ADDE([], V)", Program([], [ExpressionStatement(BuiltIn("ADDE", [DataArray([]), Numeral("V")]))]), ValList([ValInt(5)])),
# ADDE: heterogeneous types are allowed
('ADDE([I, II], "x")', Program([], [ExpressionStatement(BuiltIn("ADDE", [DataArray([Numeral("I"), Numeral("II")]), String("x")]))]), ValList([ValInt(1), ValInt(2), ValStr("x")])),
# ADDE: print form to confirm output rendering
("DIC(ADDE([I, II], III))", Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("ADDE", [DataArray([Numeral("I"), Numeral("II")]), Numeral("III")])]))]), ValStr("[I II III]"), "[I II III]\n"),
# TOLLE: remove middle element
("TOLLE([I, II, III], II)", Program([], [ExpressionStatement(BuiltIn("TOLLE", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("II")]))]), ValList([ValInt(1), ValInt(3)])),
# TOLLE: remove first element
("TOLLE([I, II, III], I)", Program([], [ExpressionStatement(BuiltIn("TOLLE", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("I")]))]), ValList([ValInt(2), ValInt(3)])),
# TOLLE: remove last element
("TOLLE([I, II, III], III)", Program([], [ExpressionStatement(BuiltIn("TOLLE", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("III")]))]), ValList([ValInt(1), ValInt(2)])),
# TOLLE: single-element array → empty
("TOLLE([V], I)", Program([], [ExpressionStatement(BuiltIn("TOLLE", [DataArray([Numeral("V")]), Numeral("I")]))]), ValList([])),
# INSERE: insert into middle
("INSERE([I, III], II, II)", Program([], [ExpressionStatement(BuiltIn("INSERE", [DataArray([Numeral("I"), Numeral("III")]), Numeral("II"), Numeral("II")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# INSERE: insert at front
("INSERE([II, III], I, I)", Program([], [ExpressionStatement(BuiltIn("INSERE", [DataArray([Numeral("II"), Numeral("III")]), Numeral("I"), Numeral("I")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# INSERE: insert at len+1 (== append)
("INSERE([I, II], III, III)", Program([], [ExpressionStatement(BuiltIn("INSERE", [DataArray([Numeral("I"), Numeral("II")]), Numeral("III"), Numeral("III")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# INSERE: into empty array at idx 1
("INSERE([], I, V)", Program([], [ExpressionStatement(BuiltIn("INSERE", [DataArray([]), Numeral("I"), Numeral("V")]))]), ValList([ValInt(5)])),
# TYPVS: integer
("TYPVS(V)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Numeral("V")]))]), ValStr("NVMERVS")),
# TYPVS: string
('TYPVS("hello")', Program([], [ExpressionStatement(BuiltIn("TYPVS", [String("hello")]))]), ValStr("LITTERA")),
# TYPVS: boolean
("TYPVS(VERITAS)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Bool(True)]))]), ValStr("VERAX")),
# TYPVS: list
("TYPVS([I, II])", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataArray([Numeral("I"), Numeral("II")])]))]), ValStr("CATALOGVS")),
# TYPVS: empty list
("TYPVS([])", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataArray([])]))]), ValStr("CATALOGVS")),
# TYPVS: fraction
("CVM FRACTIO\nTYPVS(S)", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("TYPVS", [Fractio("S")]))]), ValStr("FRACTIO")),
# TYPVS: dict
("TYPVS(TABVLA {})", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataDict([])]))]), ValStr("TABVLA")),
# TYPVS: function
("TYPVS(FVNCTIO () VT { REDI(I) })", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Fvnctio([], [Redi([Numeral("I")])])]))]), ValStr("FVNCTIO")),
# TYPVS: null
("TYPVS(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Nullus()]))]), ValStr("NVLLVS")),
# LITTERA: integer → Roman numeral
("LITTERA(V)", Program([], [ExpressionStatement(BuiltIn("LITTERA", [Numeral("V")]))]), ValStr("V")),
# LITTERA: larger integer
("LITTERA(MCMLXXXIV)", Program([], [ExpressionStatement(BuiltIn("LITTERA", [Numeral("MCMLXXXIV")]))]), ValStr("MCMLXXXIV")),
# LITTERA: zero → NVLLVS
("LITTERA(I - I)", Program([], [ExpressionStatement(BuiltIn("LITTERA", [BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS")]))]), ValStr("NVLLVS")),
# LITTERA: string passthrough
('LITTERA("salve")', Program([], [ExpressionStatement(BuiltIn("LITTERA", [String("salve")]))]), ValStr("salve")),
# LITTERA: empty string
('LITTERA("")', Program([], [ExpressionStatement(BuiltIn("LITTERA", [String("")]))]), ValStr("")),
# LITTERA: VERITAS
("LITTERA(VERITAS)", Program([], [ExpressionStatement(BuiltIn("LITTERA", [Bool(True)]))]), ValStr("VERITAS")),
# LITTERA: FALSITAS
("LITTERA(FALSITAS)", Program([], [ExpressionStatement(BuiltIn("LITTERA", [Bool(False)]))]), ValStr("FALSITAS")),
# LITTERA: NVLLVS
("LITTERA(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("LITTERA", [Nullus()]))]), ValStr("NVLLVS")),
# LITTERA: array of integers
("LITTERA([I, II, III])", Program([], [ExpressionStatement(BuiltIn("LITTERA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValStr("[I II III]")),
# LITTERA: empty array
("LITTERA([])", Program([], [ExpressionStatement(BuiltIn("LITTERA", [DataArray([])]))]), ValStr("[]")),
# LITTERA: dict with string keys (make_string emits bare keys, not quoted — matches DIC's output)
('LITTERA(TABVLA {"a" VT I})', Program([], [ExpressionStatement(BuiltIn("LITTERA", [DataDict([(String("a"), Numeral("I"))])]))]), ValStr('{a VT I}')),
# LITTERA: fraction (requires FRACTIO module)
("CVM FRACTIO\nLITTERA(S)", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("LITTERA", [Fractio("S")]))]), ValStr("S")),
# LITTERA: negative integer
("CVM SVBNVLLA\nLITTERA(-III)", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("LITTERA", [UnaryMinus(Numeral("III"))]))]), ValStr("-III")),
# LITTERA: round-trips through NVMERVS
('NVMERVS(LITTERA(VII))', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [BuiltIn("LITTERA", [Numeral("VII")])]))]), ValInt(7)),
# LITTERA: concatenated with a string via &
('LITTERA(V) & " est quinque"', Program([], [ExpressionStatement(BinOp(BuiltIn("LITTERA", [Numeral("V")]), String(" est quinque"), "SYMBOL_AMPERSAND"))]), ValStr("V est quinque")),
# LITTERA: via variable
("DESIGNA x VT IX\nLITTERA(x)", Program([], [Designa(ID("x"), Numeral("IX")), ExpressionStatement(BuiltIn("LITTERA", [ID("x")]))]), ValStr("IX")),
# QVAERE: basic literal match
('QVAERE("ab", "abcabc")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("ab"), String("abcabc")]))]), ValList([ValStr("ab"), ValStr("ab")])),
# QVAERE: no match → empty list
('QVAERE("xyz", "abc")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("xyz"), String("abc")]))]), ValList([])),
# QVAERE: regex character class
('QVAERE("[a-z]+", "abc123def")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("[a-z]+"), String("abc123def")]))]), ValList([ValStr("abc"), ValStr("def")])),
# QVAERE: empty text → empty list
('QVAERE("a", "")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("a"), String("")]))]), ValList([])),
# QVAERE: capture groups still return full match
('QVAERE("(a)(b)", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("(a)(b)"), String("ab")]))]), ValList([ValStr("ab")])),
# QVAERE: empty pattern matches between every character
('QVAERE("", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String(""), String("ab")]))]), ValList([ValStr(""), ValStr(""), ValStr("")])),
# QVAERE: dot matches any character
('QVAERE(".", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("."), String("ab")]))]), ValList([ValStr("a"), ValStr("b")])),
# SVBSTITVE: basic literal replacement
('SVBSTITVE("a", "b", "aaa")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String("b"), String("aaa")]))]), ValStr("bbb")),
# SVBSTITVE: regex character class
('SVBSTITVE("[0-9]+", "N", "abc123def456")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("[0-9]+"), String("N"), String("abc123def456")]))]), ValStr("abcNdefN")),
# SVBSTITVE: no match → string unchanged
('SVBSTITVE("x", "y", "abc")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("x"), String("y"), String("abc")]))]), ValStr("abc")),
# SVBSTITVE: empty replacement (deletion)
('SVBSTITVE("a", "", "banana")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String(""), String("banana")]))]), ValStr("bnn")),
# SVBSTITVE: empty text → empty string
('SVBSTITVE("a", "b", "")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String("b"), String("")]))]), ValStr("")),
# SVBSTITVE: dot matches any character
('SVBSTITVE(".", "x", "ab")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("."), String("x"), String("ab")]))]), ValStr("xx")),
# SVBSTITVE: backreference swaps two groups (Roman numerals)
('SVBSTITVE("(a)(b)", "\\II\\I", "ab")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("(a)(b)"), String("\\II\\I"), String("ab")]))]), ValStr("ba")),
# SVBSTITVE: backreference with unmatched group (ignored)
('SVBSTITVE("(a)(b)?", "\\I\\II", "a")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("(a)(b)?"), String("\\I\\II"), String("a")]))]), ValStr("a")),
# SVBSTITVE: Roman numeral quantifier in pattern
("SVBSTITVE('a{III}', 'x', 'aaa')", Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a{III}"), String("x"), String("aaa")]))]), ValStr("x")),
# QVAERE: Roman numeral quantifier — exact repetition
("QVAERE('a{III}', 'aaaa')", Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("a{III}"), String("aaaa")]))]), ValList([ValStr("aaa")])),
# QVAERE: Roman numeral quantifier — range
("QVAERE('a{II,III}', 'aaaaaa')", Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("a{II,III}"), String("aaaaaa")]))]), ValList([ValStr("aaa"), ValStr("aaa")])),
# QVAERE: Roman numeral quantifier — at-least
("QVAERE('a{II,}', 'a aa aaa')", Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("a{II,}"), String("a aa aaa")]))]), ValList([ValStr("aa"), ValStr("aaa")])),
# QVAERE: pattern backreference — repeated character
("QVAERE('(.)\\I', 'aabcdd')", Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("(.)\\I"), String("aabcdd")]))]), ValList([ValStr("aa"), ValStr("dd")])),
# QVAERE: pattern backreference — repeated group
("QVAERE('(..)\\I', 'ababcc')", Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("(..)\\I"), String("ababcc")]))]), ValList([ValStr("abab")])),
# NVMERVS: basic conversion
('NVMERVS("XIV")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("XIV")]))]), ValInt(14)),
# NVMERVS: simple single numeral
('NVMERVS("I")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("I")]))]), ValInt(1)),
# NVMERVS: large numeral
('NVMERVS("MMMCMXCIX")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("MMMCMXCIX")]))]), ValInt(3999)),
# NVMERVS: subtractive form
('NVMERVS("IX")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("IX")]))]), ValInt(9)),
# SCINDE: basic split
('SCINDE("a,b,c", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("a,b,c"), String(",")]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])),
# SCINDE: no match (delimiter not found)
('SCINDE("abc", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("abc"), String(",")]))]), ValList([ValStr("abc")])),
# SCINDE: empty string
('SCINDE("", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String(""), String(",")]))]), ValList([ValStr("")])),
# SCINDE: multi-char delimiter
('SCINDE("a::b::c", "::")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("a::b::c"), String("::")]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])),
# SCINDE: delimiter at edges
('SCINDE(",a,", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String(",a,"), String(",")]))]), ValList([ValStr(""), ValStr("a"), ValStr("")])),
# SCINDE: empty delimiter (split into chars)
('SCINDE("abc", "")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("abc"), String("")]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])),
# MAIVSCVLA: basic lowercase→uppercase
('MAIVSCVLA("hello")', Program([], [ExpressionStatement(BuiltIn("MAIVSCVLA", [String("hello")]))]), ValStr("HELLO")),
# MAIVSCVLA: mixed case
('MAIVSCVLA("HeLLo")', Program([], [ExpressionStatement(BuiltIn("MAIVSCVLA", [String("HeLLo")]))]), ValStr("HELLO")),
# MAIVSCVLA: already uppercase (idempotence)
('MAIVSCVLA("HELLO")', Program([], [ExpressionStatement(BuiltIn("MAIVSCVLA", [String("HELLO")]))]), ValStr("HELLO")),
# MAIVSCVLA: empty string
('MAIVSCVLA("")', Program([], [ExpressionStatement(BuiltIn("MAIVSCVLA", [String("")]))]), ValStr("")),
# MAIVSCVLA: Roman-numeral-shaped ASCII (case-only, not numeral-aware)
('MAIVSCVLA("xii")', Program([], [ExpressionStatement(BuiltIn("MAIVSCVLA", [String("xii")]))]), ValStr("XII")),
# MAIVSCVLA: non-alphabetic chars unchanged
('MAIVSCVLA("a,b!1")', Program([], [ExpressionStatement(BuiltIn("MAIVSCVLA", [String("a,b!1")]))]), ValStr("A,B!1")),
# MAIVSCVLA: via variable
('DESIGNA s VT "foo"\nDIC(MAIVSCVLA(s))', Program([], [Designa(ID("s"), String("foo")), ExpressionStatement(BuiltIn("DIC", [BuiltIn("MAIVSCVLA", [ID("s")])]))]), ValStr("FOO"), "FOO\n"),
# MAIVSCVLA: concatenated with &
('MAIVSCVLA("hi") & "!"', Program([], [ExpressionStatement(BinOp(BuiltIn("MAIVSCVLA", [String("hi")]), String("!"), "SYMBOL_AMPERSAND"))]), ValStr("HI!")),
# MINVSCVLA: basic uppercase→lowercase
('MINVSCVLA("HELLO")', Program([], [ExpressionStatement(BuiltIn("MINVSCVLA", [String("HELLO")]))]), ValStr("hello")),
# MINVSCVLA: mixed case
('MINVSCVLA("HeLLo")', Program([], [ExpressionStatement(BuiltIn("MINVSCVLA", [String("HeLLo")]))]), ValStr("hello")),
# MINVSCVLA: already lowercase (idempotence)
('MINVSCVLA("hello")', Program([], [ExpressionStatement(BuiltIn("MINVSCVLA", [String("hello")]))]), ValStr("hello")),
# MINVSCVLA: empty string
('MINVSCVLA("")', Program([], [ExpressionStatement(BuiltIn("MINVSCVLA", [String("")]))]), ValStr("")),
# MINVSCVLA: Roman-numeral-shaped ASCII (case-only, not numeral-aware)
('MINVSCVLA("XII")', Program([], [ExpressionStatement(BuiltIn("MINVSCVLA", [String("XII")]))]), ValStr("xii")),
# MINVSCVLA: non-alphabetic chars unchanged
('MINVSCVLA("A,B!1")', Program([], [ExpressionStatement(BuiltIn("MINVSCVLA", [String("A,B!1")]))]), ValStr("a,b!1")),
# MINVSCVLA round-trips MAIVSCVLA on lowercase input
('MINVSCVLA(MAIVSCVLA("hi"))', Program([], [ExpressionStatement(BuiltIn("MINVSCVLA", [BuiltIn("MAIVSCVLA", [String("hi")])]))]), ValStr("hi")),
# NECTE: zip two integer arrays
("NECTE([I, II, III], [IV, V, VI])", Program([], [ExpressionStatement(BuiltIn("NECTE", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), DataArray([Numeral("IV"), Numeral("V"), Numeral("VI")])]))]), ValList([ValList([ValInt(1), ValInt(4)]), ValList([ValInt(2), ValInt(5)]), ValList([ValInt(3), ValInt(6)])])),
# NECTE: empty + empty
("NECTE([], [])", Program([], [ExpressionStatement(BuiltIn("NECTE", [DataArray([]), DataArray([])]))]), ValList([])),
# NECTE: single element
("NECTE([I], [II])", Program([], [ExpressionStatement(BuiltIn("NECTE", [DataArray([Numeral("I")]), DataArray([Numeral("II")])]))]), ValList([ValList([ValInt(1), ValInt(2)])])),
# NECTE: mixed types (numerals paired with strings)
('NECTE([I, II], ["a", "b"])', Program([], [ExpressionStatement(BuiltIn("NECTE", [DataArray([Numeral("I"), Numeral("II")]), DataArray([String("a"), String("b")])]))]), ValList([ValList([ValInt(1), ValStr("a")]), ValList([ValInt(2), ValStr("b")])])),
# NECTE: print form
('DIC(NECTE([I, II], [III, IV]))', Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("NECTE", [DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])])]))]), ValStr("[[I III] [II IV]]"), "[[I III] [II IV]]\n"),
# NECTE: via variables
("DESIGNA a VT [I, II]\nDESIGNA b VT [III, IV]\nNECTE(a, b)", Program([], [Designa(ID("a"), DataArray([Numeral("I"), Numeral("II")])), Designa(ID("b"), DataArray([Numeral("III"), Numeral("IV")])), ExpressionStatement(BuiltIn("NECTE", [ID("a"), ID("b")]))]), ValList([ValList([ValInt(1), ValInt(3)]), ValList([ValInt(2), ValInt(4)])])),
# IVNGE: string keys
('IVNGE(["a", "b"], [I, II])', Program([], [ExpressionStatement(BuiltIn("IVNGE", [DataArray([String("a"), String("b")]), DataArray([Numeral("I"), Numeral("II")])]))]), ValDict({"a": ValInt(1), "b": ValInt(2)})),
# IVNGE: integer keys
('IVNGE([I, II], ["one", "two"])', Program([], [ExpressionStatement(BuiltIn("IVNGE", [DataArray([Numeral("I"), Numeral("II")]), DataArray([String("one"), String("two")])]))]), ValDict({1: ValStr("one"), 2: ValStr("two")})),
# IVNGE: empty + empty
("IVNGE([], [])", Program([], [ExpressionStatement(BuiltIn("IVNGE", [DataArray([]), DataArray([])]))]), ValDict({})),
# IVNGE: duplicate keys → last wins
('IVNGE(["a", "a"], [I, II])', Program([], [ExpressionStatement(BuiltIn("IVNGE", [DataArray([String("a"), String("a")]), DataArray([Numeral("I"), Numeral("II")])]))]), ValDict({"a": ValInt(2)})),
# IVNGE: print form
('DIC(IVNGE(["a", "b"], [I, II]))', Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("IVNGE", [DataArray([String("a"), String("b")]), DataArray([Numeral("I"), Numeral("II")])])]))]), ValStr("{a VT I, b VT II}"), "{a VT I, b VT II}\n"),
# IVNGE composes with CLAVES (round-trip keys)
('CLAVES(IVNGE(["a", "b"], [I, II]))', Program([], [ExpressionStatement(BuiltIn("CLAVES", [BuiltIn("IVNGE", [DataArray([String("a"), String("b")]), DataArray([Numeral("I"), Numeral("II")])])]))]), ValList([ValStr("a"), ValStr("b")])),
]
class TestBuiltins(unittest.TestCase):
@parameterized.expand(builtin_tests)
def test_builtins(self, source, nodes, value, output="", input_lines=[]):
run_test(self, source, nodes, value, output, input_lines)

175
tests/04_test_numerals.py Normal file
View File

@@ -0,0 +1,175 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Roman numeral utilities ---
class TestNumerals(unittest.TestCase):
# num_to_int: valid cases
def test_simple_numerals(self):
for s, n in [("I",1),("V",5),("X",10),("L",50),("C",100),("D",500),("M",1000)]:
self.assertEqual(num_to_int(s, False), n)
def test_subtractive_forms(self):
for s, n in [("IV",4),("IX",9),("XL",40),("XC",90),("CD",400),("CM",900)]:
self.assertEqual(num_to_int(s, False), n)
def test_complex_numerals(self):
for s, n in [("XLII",42),("XCIX",99),("MCMXCIX",1999),("MMMCMXCIX",3999)]:
self.assertEqual(num_to_int(s, False), n)
# num_to_int: invalid cases
def test_four_in_a_row_raises(self):
with self.assertRaises(Exception):
num_to_int("IIII", False)
def test_four_x_in_a_row_raises(self):
with self.assertRaises(Exception):
num_to_int("XXXX", False)
def test_invalid_subtractive_iix_raises(self):
# IIX is non-standard — I can't appear twice before X
with self.assertRaises(Exception):
num_to_int("IIX", False)
def test_invalid_subtractive_im_raises(self):
# I can only subtract from V and X, not M
with self.assertRaises(Exception):
num_to_int("IM", False)
def test_negative_without_svbnvlla_raises(self):
with self.assertRaises(CentvrionError):
num_to_int("-IV", False)
def test_negative_with_svbnvlla(self):
self.assertEqual(num_to_int("-IV", False, True), -4)
self.assertEqual(num_to_int("-XLII", False, True), -42)
# int_to_num: valid cases
def test_int_to_num(self):
for n, s in [(0,"NVLLVS"),(1,"I"),(4,"IV"),(9,"IX"),(40,"XL"),(42,"XLII"),(3999,"MMMCMXCIX")]:
self.assertEqual(int_to_num(n, False), s)
def test_int_to_num_above_3999_raises(self):
with self.assertRaises(Exception):
int_to_num(4000, False)
def test_int_to_num_magnvm(self):
# 4000 with MAGNVM enabled
self.assertEqual(int_to_num(4000, True), "MV_")
def test_num_to_int_magnvm_required(self):
# Numbers parsed from strings with _ require MAGNVM
with self.assertRaises(Exception):
num_to_int("V_", False)
# --- Arithmetic: edge cases ---
arithmetic_edge_tests = [
("I - I", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS"))]), ValInt(0)), # result zero
("I - V", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("V"), "SYMBOL_MINUS"))]), ValInt(-4)), # negative result
("I / V", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("V"), "SYMBOL_DIVIDE"))]), ValInt(0)), # integer division → 0
("M * M", Program([], [ExpressionStatement(BinOp(Numeral("M"), Numeral("M"), "SYMBOL_TIMES"))]), ValInt(1000000)), # large intermediate (not displayed)
("(I + II) * (IV - I)", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"), BinOp(Numeral("IV"), Numeral("I"), "SYMBOL_MINUS"), "SYMBOL_TIMES"))]), ValInt(9)), # nested parens
# NVLLVS coerces to 0 in integer arithmetic
("NVLLVS + V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "SYMBOL_PLUS"))]), ValInt(5)),
("V + NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "SYMBOL_PLUS"))]), ValInt(5)),
("NVLLVS + NVLLVS", Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_PLUS"))]), ValNul()),
("NVLLVS - V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "SYMBOL_MINUS"))]), ValInt(-5)),
("V - NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "SYMBOL_MINUS"))]), ValInt(5)),
# NVLLVS coerces to 0 in modulo and division
("NVLLVS RELIQVVM V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "KEYWORD_RELIQVVM"))]), ValInt(0)),
("NVLLVS / V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "SYMBOL_DIVIDE"))]), ValInt(0)),
# floored division and modulo with negative operands (Python semantics)
("CVM SVBNVLLA\n- VII RELIQVVM III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("VII")), Numeral("III"), "KEYWORD_RELIQVVM"))]), ValInt(2)),
("CVM SVBNVLLA\nVII RELIQVVM - III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(Numeral("VII"), UnaryMinus(Numeral("III")), "KEYWORD_RELIQVVM"))]), ValInt(-2)),
("CVM SVBNVLLA\n- VII RELIQVVM - III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("VII")), UnaryMinus(Numeral("III")), "KEYWORD_RELIQVVM"))]), ValInt(-1)),
("CVM SVBNVLLA\n- VII / III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("VII")), Numeral("III"), "SYMBOL_DIVIDE"))]), ValInt(-3)),
("CVM SVBNVLLA\nVII / - III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(Numeral("VII"), UnaryMinus(Numeral("III")), "SYMBOL_DIVIDE"))]), ValInt(-3)),
("CVM SVBNVLLA\n- VII / - III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("VII")), UnaryMinus(Numeral("III")), "SYMBOL_DIVIDE"))]), ValInt(2)),
("CVM SVBNVLLA\n- L RELIQVVM C", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("L")), Numeral("C"), "KEYWORD_RELIQVVM"))]), ValInt(50)),
]
class TestArithmeticEdge(unittest.TestCase):
@parameterized.expand(arithmetic_edge_tests)
def test_arithmetic_edge(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- MAGNVM module ---
# (ValueError for 4000 without MAGNVM is already in error_tests)
magnvm_tests = [
# M+M+M+M = 4000; MAGNVM allows display as "MV_"
("CVM MAGNVM\nDIC(M + M + M + M)",
Program([ModuleCall("MAGNVM")], [ExpressionStatement(BuiltIn("DIC", [BinOp(BinOp(BinOp(Numeral("M"), Numeral("M"), "SYMBOL_PLUS"), Numeral("M"), "SYMBOL_PLUS"), Numeral("M"), "SYMBOL_PLUS")]))]),
ValStr("MV_"), "MV_\n"),
# I_ = 1000 with MAGNVM (same value as M, but written with thousands operator)
("CVM MAGNVM\nI_",
Program([ModuleCall("MAGNVM")], [ExpressionStatement(Numeral("I_"))]),
ValInt(1000), ""),
# I_ + I_ = 2000; displayed as MM with MAGNVM
("CVM MAGNVM\nDIC(I_ + I_)",
Program([ModuleCall("MAGNVM")], [ExpressionStatement(BuiltIn("DIC", [BinOp(Numeral("I_"), Numeral("I_"), "SYMBOL_PLUS")]))]),
ValStr("MM"), "MM\n"),
]
class TestMAGNVM(unittest.TestCase):
@parameterized.expand(magnvm_tests)
def test_magnvm(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- SVBNVLLA module (display of negatives) ---
svbnvlla_display_tests = [
# DIC prints a negative numeral
("CVM SVBNVLLA\nDIC(- III)",
Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [UnaryMinus(Numeral("III"))]))]),
ValStr("-III"), "-III\n"),
# Concat operator & with a negative numeral
('CVM SVBNVLLA\nDIC("x: " & - V)',
Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [BinOp(String("x: "), UnaryMinus(Numeral("V")), "SYMBOL_AMPERSAND")]))]),
ValStr("x: -V"), "x: -V\n"),
# String interpolation of a negative numeral
('CVM SVBNVLLA\nDIC("val: {- X}")',
Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [InterpolatedString([String("val: "), UnaryMinus(Numeral("X"))])]))]),
ValStr("val: -X"), "val: -X\n"),
# DIC of LITTERA(negative numeral)
("CVM SVBNVLLA\nDIC(LITTERA(- III))",
Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("LITTERA", [UnaryMinus(Numeral("III"))])]))]),
ValStr("-III"), "-III\n"),
# Combined with MAGNVM: negative of a number > 3999
("CVM MAGNVM\nCVM SVBNVLLA\nDIC(- (M + M + M + M))",
Program([ModuleCall("MAGNVM"), ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [UnaryMinus(BinOp(BinOp(BinOp(Numeral("M"), Numeral("M"), "SYMBOL_PLUS"), Numeral("M"), "SYMBOL_PLUS"), Numeral("M"), "SYMBOL_PLUS"))]))]),
ValStr("-MV_"), "-MV_\n"),
# Negative fraction via int / -int
("CVM FRACTIO\nCVM SVBNVLLA\nDIC(V / - II)",
Program([ModuleCall("FRACTIO"), ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [BinOp(Numeral("V"), UnaryMinus(Numeral("II")), "SYMBOL_DIVIDE")]))]),
ValStr("-IIS"), "-IIS\n"),
# Negative pure fraction (no integer part)
("CVM FRACTIO\nCVM SVBNVLLA\nDIC(I / - II)",
Program([ModuleCall("FRACTIO"), ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [BinOp(Numeral("I"), UnaryMinus(Numeral("II")), "SYMBOL_DIVIDE")]))]),
ValStr("-S"), "-S\n"),
("CVM FRACTIO\nCVM SVBNVLLA\nDIC(- S)",
Program([ModuleCall("FRACTIO"), ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [UnaryMinus(Fractio("S"))]))]),
ValStr("-S"), "-S\n"),
("CVM FRACTIO\nCVM SVBNVLLA\nDIC(- IIS)",
Program([ModuleCall("FRACTIO"), ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("DIC", [UnaryMinus(Fractio("IIS"))]))]),
ValStr("-IIS"), "-IIS\n"),
]
class TestSVBNVLLADisplay(unittest.TestCase):
@parameterized.expand(svbnvlla_display_tests)
def test_svbnvlla_display(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)

402
tests/05_test_literals.py Normal file
View File

@@ -0,0 +1,402 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Repr ---
repr_tests = [
("string", String("hello"), "String(hello)"),
("numeral", Numeral("III"), "Numeral(III)"),
("bool_true", Bool(True), "Bool(True)"),
("bool_false", Bool(False), "Bool(False)"),
("nullus", Nullus(), "Nullus()"),
("erumpe", Erumpe(), "Erumpe()"),
("module_call", ModuleCall("FORS"), "FORS"),
("id", ID("x"), "ID(x)"),
("expression_stmt", ExpressionStatement(String("hi")), "ExpressionStatement(\n String(hi)\n)"),
("data_array", DataArray([Numeral("I"), Numeral("II")]), "Array([\n Numeral(I),\n Numeral(II)\n])"),
("data_range_array", DataRangeArray(Numeral("I"), Numeral("X")), "RangeArray([\n Numeral(I),\n Numeral(X)\n])"),
("designa", Designa(ID("x"), Numeral("III")), "Designa(\n ID(x),\n Numeral(III)\n)"),
("binop", BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"), "BinOp(\n Numeral(I),\n Numeral(II),\n SYMBOL_PLUS\n)"),
("redi", Redi([Numeral("I")]), "Redi([\n Numeral(I)\n])"),
("si_no_else", SiStatement(Bool(True), [ExpressionStatement(Erumpe())], None), "Si(\n Bool(True),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ]),\n None\n)"),
("si_with_else", SiStatement(Bool(True), [ExpressionStatement(Erumpe())], [ExpressionStatement(Erumpe())]), "Si(\n Bool(True),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ]),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ])\n)"),
("si_empty_else", SiStatement(Bool(True), [ExpressionStatement(Erumpe())], []), "Si(\n Bool(True),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ]),\n statements([])\n)"),
("dum", DumStatement(Bool(False), [ExpressionStatement(Erumpe())]), "Dum(\n Bool(False),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ])\n)"),
("per", PerStatement(DataArray([Numeral("I")]), ID("i"), [ExpressionStatement(Erumpe())]), "Per(\n Array([\n Numeral(I)\n ]),\n ID(i),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ])\n)"),
("invoca", Invoca(ID("f"), [Numeral("I")]), "Invoca(\n ID(f),\n parameters([\n Numeral(I)\n ])\n)"),
("builtin", BuiltIn("DIC", [String("hi")]), "Builtin(\n DIC,\n parameters([\n String(hi)\n ])\n)"),
("defini", Defini(ID("f"), [ID("n")], [ExpressionStatement(Redi([Numeral("I")]))]), "Defini(\n ID(f),\n parameters([\n ID(n)\n ]),\n statements([\n ExpressionStatement(\n Redi([\n Numeral(I)\n ])\n )\n ])\n)"),
("program_no_modules", Program([], [ExpressionStatement(Numeral("I"))]), "modules([]),\nstatements([\n ExpressionStatement(\n Numeral(I)\n )\n])"),
("program_with_module", Program([ModuleCall("FORS")], [ExpressionStatement(Numeral("I"))]), "modules([\n FORS\n]),\nstatements([\n ExpressionStatement(\n Numeral(I)\n )\n])"),
]
class TestRepr(unittest.TestCase):
@parameterized.expand(repr_tests)
def test_repr(self, _, node, expected):
self.assertEqual(repr(node), expected)
# --- make_string ---
class TestMakeString(unittest.TestCase):
def test_str(self):
self.assertEqual(make_string(ValStr("hello")), "hello")
def test_int(self):
self.assertEqual(make_string(ValInt(3)), "III")
def test_int_zero(self):
self.assertEqual(make_string(ValInt(0)), "NVLLVS")
def test_bool_true(self):
self.assertEqual(make_string(ValBool(True)), "VERITAS")
def test_bool_false(self):
self.assertEqual(make_string(ValBool(False)), "FALSITAS")
def test_nul(self):
self.assertEqual(make_string(ValNul()), "NVLLVS")
def test_list(self):
self.assertEqual(make_string(ValList([ValInt(1), ValInt(2)])), "[I II]")
def test_empty_list(self):
self.assertEqual(make_string(ValList([])), "[]")
def test_nested_list(self):
self.assertEqual(
make_string(ValList([ValStr("a"), ValBool(True)])),
"[a VERITAS]"
)
# --- DIC with non-integer types ---
dic_type_tests = [
("DIC(VERITAS)", Program([], [ExpressionStatement(BuiltIn("DIC", [Bool(True)]))]), ValStr("VERITAS"), "VERITAS\n"),
("DIC(FALSITAS)", Program([], [ExpressionStatement(BuiltIn("DIC", [Bool(False)]))]), ValStr("FALSITAS"), "FALSITAS\n"),
("DIC(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("DIC", [Nullus()]))]), ValStr("NVLLVS"), "NVLLVS\n"),
('DIC([I, II])', Program([], [ExpressionStatement(BuiltIn("DIC", [DataArray([Numeral("I"), Numeral("II")])]))]), ValStr("[I II]"), "[I II]\n"),
('DIC("")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("")]))]), ValStr(""), "\n"),
# arithmetic result printed as numeral
("DIC(II + III)", Program([], [ExpressionStatement(BuiltIn("DIC", [BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS")]))]), ValStr("V"), "V\n"),
# integer 0 prints as NVLLVS
("DIC(I - I)", Program([], [ExpressionStatement(BuiltIn("DIC", [BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS")]))]), ValStr("NVLLVS"), "NVLLVS\n"),
# multiple args of mixed types
('DIC("x", VERITAS)', Program([], [ExpressionStatement(BuiltIn("DIC", [String("x"), Bool(True)]))]), ValStr("x VERITAS"), "x VERITAS\n"),
]
class TestDicTypes(unittest.TestCase):
@parameterized.expand(dic_type_tests)
def test_dic_types(self, source, nodes, value, output):
run_test(self, source, nodes, value, output)
# --- String concatenation ---
string_concat_tests = [
('"hello" & " world"', Program([], [ExpressionStatement(BinOp(String("hello"), String(" world"), "SYMBOL_AMPERSAND"))]), ValStr("hello world")),
# NVLLVS coerces to "" in string context
('NVLLVS & "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_AMPERSAND"))]), ValStr("hello")),
('"hello" & NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("hello")),
('NVLLVS & NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("")),
# integers coerce to Roman numerals in string context
('"value: " & V', Program([], [ExpressionStatement(BinOp(String("value: "), Numeral("V"), "SYMBOL_AMPERSAND"))]), ValStr("value: V")),
('X & " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_AMPERSAND"))]), ValStr("X items")),
]
class TestStringConcat(unittest.TestCase):
@parameterized.expand(string_concat_tests)
def test_string_concat(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- String interpolation ---
interpolation_tests = [
# basic variable interpolation
('DESIGNA nomen VT "Marcus"\n"Salve, {nomen}!"',
Program([], [
Designa(ID("nomen"), String("Marcus")),
ExpressionStatement(InterpolatedString([String("Salve, "), ID("nomen"), String("!")]))
]), ValStr("Salve, Marcus!")),
# arithmetic expression inside interpolation
('DESIGNA x VT III\n"Sum: {x + II}"',
Program([], [
Designa(ID("x"), Numeral("III")),
ExpressionStatement(InterpolatedString([String("Sum: "), BinOp(ID("x"), Numeral("II"), "SYMBOL_PLUS")]))
]), ValStr("Sum: V")),
# multiple interpolations
('DESIGNA a VT I\nDESIGNA b VT II\n"{a} + {b} = {a + b}"',
Program([], [
Designa(ID("a"), Numeral("I")),
Designa(ID("b"), Numeral("II")),
ExpressionStatement(InterpolatedString([
ID("a"), String(" + "), ID("b"), String(" = "),
BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"),
]))
]), ValStr("I + II = III")),
# escaped braces become literal
('"use {{braces}}"',
Program([], [ExpressionStatement(String("use {braces}"))]),
ValStr("use {braces}")),
# single-quoted strings ignore braces
("'hello {world}'",
Program([], [ExpressionStatement(String("hello {world}"))]),
ValStr("hello {world}")),
# integer coercion
('DESIGNA n VT V\n"n is {n}"',
Program([], [
Designa(ID("n"), Numeral("V")),
ExpressionStatement(InterpolatedString([String("n is "), ID("n")]))
]), ValStr("n is V")),
# boolean coercion
('DESIGNA b VT VERITAS\n"value: {b}"',
Program([], [
Designa(ID("b"), Bool(True)),
ExpressionStatement(InterpolatedString([String("value: "), ID("b")]))
]), ValStr("value: VERITAS")),
# NVLLVS coercion
('"value: {NVLLVS}"',
Program([], [
ExpressionStatement(InterpolatedString([String("value: "), Nullus()]))
]), ValStr("value: NVLLVS")),
# integer 0 interpolates as NVLLVS
('"value: {I - I}"', Program([], [ExpressionStatement(InterpolatedString([String("value: "), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS")]))]), ValStr("value: NVLLVS")),
# expression-only string (no literal parts around it)
('DESIGNA x VT "hi"\n"{x}"',
Program([], [
Designa(ID("x"), String("hi")),
ExpressionStatement(InterpolatedString([ID("x")]))
]), ValStr("hi")),
# adjacent interpolations
('DESIGNA a VT "x"\nDESIGNA b VT "y"\n"{a}{b}"',
Program([], [
Designa(ID("a"), String("x")),
Designa(ID("b"), String("y")),
ExpressionStatement(InterpolatedString([ID("a"), ID("b")]))
]), ValStr("xy")),
# function call inside interpolation
("DEFINI f () VT {\nREDI (V)\n}\n\"result: {INVOCA f()}\"",
Program([], [
Defini(ID("f"), [], [Redi([Numeral("V")])]),
ExpressionStatement(InterpolatedString([String("result: "), Invoca(ID("f"), [])]))
]), ValStr("result: V")),
# single-quoted string inside interpolation
("DESIGNA x VT 'hello'\n\"{x & '!'}\"",
Program([], [
Designa(ID("x"), String("hello")),
ExpressionStatement(InterpolatedString([BinOp(ID("x"), String("!"), "SYMBOL_AMPERSAND")]))
]), ValStr("hello!")),
# plain double-quoted string (no braces) still works
('"hello world"',
Program([], [ExpressionStatement(String("hello world"))]),
ValStr("hello world")),
# interpolation in DIC output
('DESIGNA name VT "Roma"\nDIC("Salve, {name}!")',
Program([], [
Designa(ID("name"), String("Roma")),
ExpressionStatement(BuiltIn("DIC", [InterpolatedString([String("Salve, "), ID("name"), String("!")])]))
]), ValStr("Salve, Roma!"), "Salve, Roma!\n"),
]
class TestInterpolation(unittest.TestCase):
@parameterized.expand(interpolation_tests)
def test_interpolation(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- Escape sequences ---
escape_tests = [
# \n → newline
('"hello\\nworld"',
Program([], [ExpressionStatement(String("hello\nworld"))]),
ValStr("hello\nworld")),
# \t → tab
('"col\\tcol"',
Program([], [ExpressionStatement(String("col\tcol"))]),
ValStr("col\tcol")),
# \r → carriage return
('"line\\rover"',
Program([], [ExpressionStatement(String("line\rover"))]),
ValStr("line\rover")),
# \\ → literal backslash
('"back\\\\slash"',
Program([], [ExpressionStatement(String("back\\slash"))]),
ValStr("back\\slash")),
# \" → literal double quote
('"say \\"salve\\""',
Program([], [ExpressionStatement(String('say "salve"'))]),
ValStr('say "salve"')),
# \' → literal single quote in single-quoted string
("'it\\'s'",
Program([], [ExpressionStatement(String("it's"))]),
ValStr("it's")),
# \n in single-quoted string
("'hello\\nworld'",
Program([], [ExpressionStatement(String("hello\nworld"))]),
ValStr("hello\nworld")),
# escape inside interpolated string
('DESIGNA name VT "Roma"\n"salve\\n{name}"',
Program([], [
Designa(ID("name"), String("Roma")),
ExpressionStatement(InterpolatedString([String("salve\n"), ID("name")]))
]), ValStr("salve\nRoma")),
# DIC with newline escape
('DIC("hello\\nworld")',
Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello\nworld")]))]),
ValStr("hello\nworld"), "hello\nworld\n"),
# multiple escapes in one string
('"\\t\\n\\\\"',
Program([], [ExpressionStatement(String("\t\n\\"))]),
ValStr("\t\n\\")),
# unknown escapes pass through (regex backrefs)
('"\\1\\2"',
Program([], [ExpressionStatement(String("\\1\\2"))]),
ValStr("\\1\\2")),
]
class TestEscapeSequences(unittest.TestCase):
@parameterized.expand(escape_tests)
def test_escape(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- String index assignment ---
string_index_assign_tests = [
# assign to middle character
('DESIGNA s VT "ABCDE"\nDESIGNA s[III] VT "X"\ns',
Program([], [
Designa(ID("s"), String("ABCDE")),
DesignaIndex(ID("s"), [Numeral("III")], String("X")),
ExpressionStatement(ID("s")),
]),
ValStr("ABXDE")),
# assign to first character
('DESIGNA s VT "ABCDE"\nDESIGNA s[I] VT "Z"\ns',
Program([], [
Designa(ID("s"), String("ABCDE")),
DesignaIndex(ID("s"), [Numeral("I")], String("Z")),
ExpressionStatement(ID("s")),
]),
ValStr("ZBCDE")),
# assign to last character
('DESIGNA s VT "ABCDE"\nDESIGNA s[V] VT "Z"\ns',
Program([], [
Designa(ID("s"), String("ABCDE")),
DesignaIndex(ID("s"), [Numeral("V")], String("Z")),
ExpressionStatement(ID("s")),
]),
ValStr("ABCDZ")),
# variable as index
('DESIGNA s VT "ABCDE"\nDESIGNA i VT II\nDESIGNA s[i] VT "X"\ns',
Program([], [
Designa(ID("s"), String("ABCDE")),
Designa(ID("i"), Numeral("II")),
DesignaIndex(ID("s"), [ID("i")], String("X")),
ExpressionStatement(ID("s")),
]),
ValStr("AXCDE")),
# string inside array
('DESIGNA a VT ["ABC", "DEF"]\nDESIGNA a[I][II] VT "X"\na[I]',
Program([], [
Designa(ID("a"), DataArray([String("ABC"), String("DEF")])),
DesignaIndex(ID("a"), [Numeral("I"), Numeral("II")], String("X")),
ExpressionStatement(ArrayIndex(ID("a"), Numeral("I"))),
]),
ValStr("AXC")),
]
class TestStringIndexAssign(unittest.TestCase):
@parameterized.expand(string_index_assign_tests)
def test_string_index_assign(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- String indexing ---
string_index_tests = [
# first character
('"SALVTE"[I]',
Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("I")))]),
ValStr("S")),
# last character
('"SALVTE"[VI]',
Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("VI")))]),
ValStr("E")),
# middle character
('"SALVTE"[III]',
Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("III")))]),
ValStr("L")),
# string index via variable
('DESIGNA s VT "SALVTE"\ns[II]',
Program([], [
Designa(ID("s"), String("SALVTE")),
ExpressionStatement(ArrayIndex(ID("s"), Numeral("II"))),
]),
ValStr("A")),
# expression as index
('"SALVTE"[I + II]',
Program([], [ExpressionStatement(ArrayIndex(
String("SALVTE"),
BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS")))]),
ValStr("L")),
]
class TestStringIndex(unittest.TestCase):
@parameterized.expand(string_index_tests)
def test_string_index(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- String slicing ---
string_slice_tests = [
# substring from middle
('"SALVTE"[II VSQVE IV]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"), Numeral("II"), Numeral("IV")))]),
ValStr("ALV")),
# full string slice
('"SALVTE"[I VSQVE VI]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"), Numeral("I"), Numeral("VI")))]),
ValStr("SALVTE")),
# single-char slice
('"SALVTE"[III VSQVE III]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"), Numeral("III"), Numeral("III")))]),
ValStr("L")),
# slice on variable
('DESIGNA s VT "SALVTE"\ns[II VSQVE IV]',
Program([], [
Designa(ID("s"), String("SALVTE")),
ExpressionStatement(ArraySlice(ID("s"), Numeral("II"), Numeral("IV"))),
]),
ValStr("ALV")),
# chaining: slice then index
('"SALVTE"[I VSQVE III][II]',
Program([], [ExpressionStatement(ArrayIndex(
ArraySlice(String("SALVTE"), Numeral("I"), Numeral("III")),
Numeral("II")))]),
ValStr("A")),
# expression as slice bounds
('"SALVTE"[I + I VSQVE II + II]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"),
BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"),
BinOp(Numeral("II"), Numeral("II"), "SYMBOL_PLUS")))]),
ValStr("ALV")),
]
class TestStringSlice(unittest.TestCase):
@parameterized.expand(string_slice_tests)
def test_string_slice(self, source, nodes, value):
run_test(self, source, nodes, value)

223
tests/06_test_booleans.py Normal file
View File

@@ -0,0 +1,223 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Comparison operators ---
comparison_tests = [
# EST on strings
('\"hello\" EST \"hello\"', Program([], [ExpressionStatement(BinOp(String("hello"), String("hello"), "KEYWORD_EST"))]), ValBool(True)),
('\"hello\" EST \"world\"', Program([], [ExpressionStatement(BinOp(String("hello"), String("world"), "KEYWORD_EST"))]), ValBool(False)),
# chain comparisons as conditions
("SI III PLVS II TVNC { DESIGNA r VT I }\nr",
Program([], [SiStatement(BinOp(Numeral("III"), Numeral("II"), "KEYWORD_PLVS"), [Designa(ID("r"), Numeral("I"))], None), ExpressionStatement(ID("r"))]),
ValInt(1)),
("SI II PLVS III TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Numeral("II"), Numeral("III"), "KEYWORD_PLVS"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(2)),
# result of comparison is ValBool
("I EST I", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"))]), ValBool(True)),
("I EST II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"))]), ValBool(False)),
("I MINVS II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_MINVS"))]), ValBool(True)),
("II MINVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_MINVS"))]), ValBool(False)),
("II PLVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_PLVS"))]), ValBool(True)),
("I PLVS II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_PLVS"))]), ValBool(False)),
# NVLLVS coerces to 0 in comparisons
("V PLVS NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "KEYWORD_PLVS"))]), ValBool(True)),
("NVLLVS MINVS V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "KEYWORD_MINVS"))]), ValBool(True)),
# DISPAR (not-equal): mirrors EST semantics, negated
("I DISPAR II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_DISPAR"))]), ValBool(True)),
("I DISPAR I", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_DISPAR"))]), ValBool(False)),
('"hello" DISPAR "hello"', Program([], [ExpressionStatement(BinOp(String("hello"), String("hello"), "KEYWORD_DISPAR"))]), ValBool(False)),
('"hello" DISPAR "world"', Program([], [ExpressionStatement(BinOp(String("hello"), String("world"), "KEYWORD_DISPAR"))]), ValBool(True)),
("VERITAS DISPAR FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(False), "KEYWORD_DISPAR"))]), ValBool(True)),
("NVLLVS DISPAR NVLLVS", Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "KEYWORD_DISPAR"))]), ValBool(False)),
# cross-type: an int and a string are never equal
('I DISPAR "I"', Program([], [ExpressionStatement(BinOp(Numeral("I"), String("I"), "KEYWORD_DISPAR"))]), ValBool(True)),
# integer 0 equals NVLLVS
("(I - I) EST NVLLVS", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS"), Nullus(), "KEYWORD_EST"))]), ValBool(True)),
("NVLLVS EST (I - I)", Program([], [ExpressionStatement(BinOp(Nullus(), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS"), "KEYWORD_EST"))]), ValBool(True)),
("(I - I) DISPAR NVLLVS", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS"), Nullus(), "KEYWORD_DISPAR"))]), ValBool(False)),
("NVLLVS DISPAR (I - I)", Program([], [ExpressionStatement(BinOp(Nullus(), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS"), "KEYWORD_DISPAR"))]), ValBool(False)),
# non-zero integer does not equal NVLLVS
("I EST NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("I"), Nullus(), "KEYWORD_EST"))]), ValBool(False)),
("NVLLVS DISPAR I", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("I"), "KEYWORD_DISPAR"))]), ValBool(True)),
# EST / DISPAR on arrays
("[I, II] EST [I, II]", Program([], [ExpressionStatement(BinOp(DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("I"), Numeral("II")]), "KEYWORD_EST"))]), ValBool(True)),
("[I, II] EST [I, III]", Program([], [ExpressionStatement(BinOp(DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("I"), Numeral("III")]), "KEYWORD_EST"))]), ValBool(False)),
("[I, II] EST [I, II, III]", Program([], [ExpressionStatement(BinOp(DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), "KEYWORD_EST"))]), ValBool(False)),
("[] EST []", Program([], [ExpressionStatement(BinOp(DataArray([]), DataArray([]), "KEYWORD_EST"))]), ValBool(True)),
("[I, II] DISPAR [I, III]", Program([], [ExpressionStatement(BinOp(DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("I"), Numeral("III")]), "KEYWORD_DISPAR"))]), ValBool(True)),
("[I, II] DISPAR [I, II]", Program([], [ExpressionStatement(BinOp(DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("I"), Numeral("II")]), "KEYWORD_DISPAR"))]), ValBool(False)),
# HAVD_PLVS (<=) and HAVD_MINVS (>=)
("I HAVD_PLVS II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_HAVD_PLVS"))]), ValBool(True)),
("II HAVD_PLVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_HAVD_PLVS"))]), ValBool(False)),
# equality boundary — the only case that distinguishes <= from <
("II HAVD_PLVS II", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("II"), "KEYWORD_HAVD_PLVS"))]), ValBool(True)),
("II HAVD_MINVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_HAVD_MINVS"))]), ValBool(True)),
("I HAVD_MINVS II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_HAVD_MINVS"))]), ValBool(False)),
# equality boundary — the only case that distinguishes >= from >
("II HAVD_MINVS II",Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("II"), "KEYWORD_HAVD_MINVS"))]), ValBool(True)),
# NVLLVS coerces to 0
("V HAVD_MINVS NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "KEYWORD_HAVD_MINVS"))]), ValBool(True)),
("NVLLVS HAVD_PLVS V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "KEYWORD_HAVD_PLVS"))]), ValBool(True)),
("NVLLVS HAVD_PLVS NVLLVS", Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "KEYWORD_HAVD_PLVS"))]), ValBool(True)),
# precedence: * binds tighter, so II*III HAVD_PLVS VI parses as (II*III) HAVD_PLVS VI = 6 <= 6 = True
("II * III HAVD_PLVS VI",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_TIMES"), Numeral("VI"), "KEYWORD_HAVD_PLVS"))]),
ValBool(True)),
# control flow: SI ... HAVD_MINVS
("SI II HAVD_MINVS II TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Numeral("II"), Numeral("II"), "KEYWORD_HAVD_MINVS"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(1)),
]
class TestComparisons(unittest.TestCase):
@parameterized.expand(comparison_tests)
def test_comparisons(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- ET and AVT (boolean and/or) ---
et_avt_tests = [
("VERITAS ET VERITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(True), "KEYWORD_ET"))]), ValBool(True)),
("VERITAS ET FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(False), "KEYWORD_ET"))]), ValBool(False)),
("FALSITAS ET VERITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(True), "KEYWORD_ET"))]), ValBool(False)),
("FALSITAS ET FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(False), "KEYWORD_ET"))]), ValBool(False)),
("VERITAS AVT VERITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(True), "KEYWORD_AVT"))]), ValBool(True)),
("VERITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(False), "KEYWORD_AVT"))]), ValBool(True)),
("FALSITAS AVT VERITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(True), "KEYWORD_AVT"))]), ValBool(True)),
("FALSITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(False), "KEYWORD_AVT"))]), ValBool(False)),
# short-circuit behavior: combined with comparisons
("(I EST I) ET (II EST II)",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_EST"), "KEYWORD_ET"))]),
ValBool(True)),
("(I EST II) AVT (II EST II)",
Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_EST"), "KEYWORD_AVT"))]),
ValBool(True)),
# used as SI condition
("SI VERITAS ET VERITAS TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Bool(True), Bool(True), "KEYWORD_ET"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(1)),
("SI FALSITAS AVT FALSITAS TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [SiStatement(BinOp(Bool(False), Bool(False), "KEYWORD_AVT"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]),
ValInt(2)),
# short-circuit: right side not evaluated when result is determined
("VERITAS AVT NVLLVS",
Program([], [ExpressionStatement(BinOp(Bool(True), Nullus(), "KEYWORD_AVT"))]),
ValBool(True)),
("FALSITAS ET NVLLVS",
Program([], [ExpressionStatement(BinOp(Bool(False), Nullus(), "KEYWORD_ET"))]),
ValBool(False)),
# short-circuit with side-effect-prone expressions
("DESIGNA x VT NVLLVS\nSI x EST NVLLVS AVT [I, II][x] EST I TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [
Designa(ID("x"), Nullus()),
SiStatement(
BinOp(BinOp(ID("x"), Nullus(), "KEYWORD_EST"), BinOp(ArrayIndex(DataArray([Numeral("I"), Numeral("II")]), ID("x")), Numeral("I"), "KEYWORD_EST"), "KEYWORD_AVT"),
[Designa(ID("r"), Numeral("I"))],
[Designa(ID("r"), Numeral("II"))]),
ExpressionStatement(ID("r"))]),
ValInt(1)),
("DESIGNA x VT NVLLVS\nSI x DISPAR NVLLVS ET [I, II][x] EST I TVNC { DESIGNA r VT I } ALIVD { DESIGNA r VT II }\nr",
Program([], [
Designa(ID("x"), Nullus()),
SiStatement(
BinOp(BinOp(ID("x"), Nullus(), "KEYWORD_DISPAR"), BinOp(ArrayIndex(DataArray([Numeral("I"), Numeral("II")]), ID("x")), Numeral("I"), "KEYWORD_EST"), "KEYWORD_ET"),
[Designa(ID("r"), Numeral("I"))],
[Designa(ID("r"), Numeral("II"))]),
ExpressionStatement(ID("r"))]),
ValInt(2)),
]
class TestEtAvt(unittest.TestCase):
@parameterized.expand(et_avt_tests)
def test_et_avt(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- NON (boolean not) ---
non_tests = [
("NON VERITAS",
Program([], [ExpressionStatement(UnaryNot(Bool(True)))]),
ValBool(False)),
("NON FALSITAS",
Program([], [ExpressionStatement(UnaryNot(Bool(False)))]),
ValBool(True)),
("NON NON VERITAS",
Program([], [ExpressionStatement(UnaryNot(UnaryNot(Bool(True))))]),
ValBool(True)),
("DESIGNA b VT I EST II\nNON b",
Program([], [Designa(ID("b"), BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST")), ExpressionStatement(UnaryNot(ID("b")))]),
ValBool(True)),
("DESIGNA z VT I EST I\nNON z",
Program([], [Designa(ID("z"), BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST")), ExpressionStatement(UnaryNot(ID("z")))]),
ValBool(False)),
("NON VERITAS AVT FALSITAS",
Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_AVT"))]),
ValBool(False)),
("NON VERITAS EST FALSITAS",
Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_EST"))]),
ValBool(True)),
]
class TestNon(unittest.TestCase):
@parameterized.expand(non_tests)
def test_non(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Values: equality and truthiness ---
class TestValues(unittest.TestCase):
def test_valint_equality(self):
self.assertEqual(ValInt(3), ValInt(3))
self.assertNotEqual(ValInt(3), ValInt(4))
def test_valstr_equality(self):
self.assertEqual(ValStr("hi"), ValStr("hi"))
self.assertNotEqual(ValStr("hi"), ValStr("bye"))
def test_valbool_equality(self):
self.assertEqual(ValBool(True), ValBool(True))
self.assertNotEqual(ValBool(True), ValBool(False))
def test_valnul_equality(self):
self.assertEqual(ValNul(), ValNul())
def test_vallist_equality(self):
self.assertEqual(ValList([ValInt(1)]), ValList([ValInt(1)]))
self.assertNotEqual(ValList([ValInt(1)]), ValList([ValInt(2)]))
self.assertNotEqual(ValList([ValInt(1)]), ValList([]))
def test_valint_truthiness(self):
self.assertTrue(bool(ValInt(1)))
self.assertTrue(bool(ValInt(-1)))
self.assertFalse(bool(ValInt(0)))
def test_valstr_truthiness(self):
self.assertTrue(bool(ValStr("x")))
self.assertFalse(bool(ValStr("")))
def test_valbool_truthiness(self):
self.assertTrue(bool(ValBool(True)))
self.assertFalse(bool(ValBool(False)))
def test_vallist_truthiness(self):
self.assertTrue(bool(ValList([ValInt(1)])))
self.assertFalse(bool(ValList([])))
def test_cross_type_inequality(self):
self.assertNotEqual(ValInt(1), ValBool(True))
self.assertNotEqual(ValInt(0), ValNul())
self.assertNotEqual(ValStr(""), ValNul())

293
tests/07_test_arrays__.py Normal file
View File

@@ -0,0 +1,293 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Array concatenation ---
array_concat_tests = [
("[I, II] @ [III, IV]",
Program([], [ExpressionStatement(BinOp(DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")]), "SYMBOL_AT"))]),
ValList([ValInt(1), ValInt(2), ValInt(3), ValInt(4)])),
("[] @ [I]",
Program([], [ExpressionStatement(BinOp(DataArray([]), DataArray([Numeral("I")]), "SYMBOL_AT"))]),
ValList([ValInt(1)])),
("[I] @ []",
Program([], [ExpressionStatement(BinOp(DataArray([Numeral("I")]), DataArray([]), "SYMBOL_AT"))]),
ValList([ValInt(1)])),
("[] @ []",
Program([], [ExpressionStatement(BinOp(DataArray([]), DataArray([]), "SYMBOL_AT"))]),
ValList([])),
('["a"] @ [I]',
Program([], [ExpressionStatement(BinOp(DataArray([String("a")]), DataArray([Numeral("I")]), "SYMBOL_AT"))]),
ValList([ValStr("a"), ValInt(1)])),
# left-associative chaining
("[I] @ [II] @ [III]",
Program([], [ExpressionStatement(BinOp(BinOp(DataArray([Numeral("I")]), DataArray([Numeral("II")]), "SYMBOL_AT"), DataArray([Numeral("III")]), "SYMBOL_AT"))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# concat with variable
("DESIGNA a VT [I, II]\nDESIGNA b VT [III]\na @ b",
Program([], [Designa(ID("a"), DataArray([Numeral("I"), Numeral("II")])), Designa(ID("b"), DataArray([Numeral("III")])), ExpressionStatement(BinOp(ID("a"), ID("b"), "SYMBOL_AT"))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
]
class TestArrayConcat(unittest.TestCase):
@parameterized.expand(array_concat_tests)
def test_array_concat(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Array indexing ---
# Indexing is 1-based; I is the first element
array_index_tests = [
# basic indexing
("[I, II, III][I]", Program([], [ExpressionStatement(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("I")))]), ValInt(1)), # first element
("[I, II, III][II]", Program([], [ExpressionStatement(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("II")))]), ValInt(2)), # second element
("[I, II, III][III]", Program([], [ExpressionStatement(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("III")))]), ValInt(3)), # third element
# index into a variable
("DESIGNA a VT [X, XX, XXX]\na[II]",
Program([], [Designa(ID("a"), DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")])), ExpressionStatement(ArrayIndex(ID("a"), Numeral("II")))]),
ValInt(20)), # second element
# index into range array
("[I VSQVE V][II]", Program([], [ExpressionStatement(ArrayIndex(DataRangeArray(Numeral("I"), Numeral("V")), Numeral("II")))]), ValInt(2)), # second element of [1,2,3,4,5]
# expression as index
("[I, II, III][I + I]",
Program([], [ExpressionStatement(ArrayIndex(
DataArray([Numeral("I"), Numeral("II"), Numeral("III")]),
BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS")))]),
ValInt(2)),
# division result as index (no FRACTIO): IV / II = 2
("[X, XX, XXX][IV / II]",
Program([], [ExpressionStatement(ArrayIndex(
DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")]),
BinOp(Numeral("IV"), Numeral("II"), "SYMBOL_DIVIDE")))]),
ValInt(20)),
# whole-number fraction (from division) as index, with FRACTIO imported
("CVM FRACTIO\n[X, XX, XXX][IV / II]",
Program([ModuleCall("FRACTIO")], [ExpressionStatement(ArrayIndex(
DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")]),
BinOp(Numeral("IV"), Numeral("II"), "SYMBOL_DIVIDE")))]),
ValInt(20)),
]
class TestArrayIndex(unittest.TestCase):
@parameterized.expand(array_index_tests)
def test_array_index(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Array index assignment ---
array_index_assign_tests = [
# assign to middle element
("DESIGNA a VT [I, II, III]\nDESIGNA a[II] VT X\na[II]",
Program([], [
Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])),
DesignaIndex(ID("a"), [Numeral("II")], Numeral("X")),
ExpressionStatement(ArrayIndex(ID("a"), Numeral("II"))),
]),
ValInt(10)),
# assign to first element
("DESIGNA a VT [I, II, III]\nDESIGNA a[I] VT V\na[I]",
Program([], [
Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])),
DesignaIndex(ID("a"), [Numeral("I")], Numeral("V")),
ExpressionStatement(ArrayIndex(ID("a"), Numeral("I"))),
]),
ValInt(5)),
# assign to last element
("DESIGNA a VT [I, II, III]\nDESIGNA a[III] VT L\na[III]",
Program([], [
Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])),
DesignaIndex(ID("a"), [Numeral("III")], Numeral("L")),
ExpressionStatement(ArrayIndex(ID("a"), Numeral("III"))),
]),
ValInt(50)),
# other elements unaffected
("DESIGNA a VT [I, II, III]\nDESIGNA a[II] VT X\na[I]",
Program([], [
Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])),
DesignaIndex(ID("a"), [Numeral("II")], Numeral("X")),
ExpressionStatement(ArrayIndex(ID("a"), Numeral("I"))),
]),
ValInt(1)),
# expression as index
("DESIGNA a VT [I, II, III]\nDESIGNA i VT II\nDESIGNA a[i] VT X\na[II]",
Program([], [
Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])),
Designa(ID("i"), Numeral("II")),
DesignaIndex(ID("a"), [ID("i")], Numeral("X")),
ExpressionStatement(ArrayIndex(ID("a"), Numeral("II"))),
]),
ValInt(10)),
]
class TestArrayIndexAssign(unittest.TestCase):
@parameterized.expand(array_index_assign_tests)
def test_array_index_assign(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Multi-dimensional array index assignment ---
multidim_assign_tests = [
# 2D array assignment
("DESIGNA a VT [[I, II], [III, IV]]\nDESIGNA a[I][II] VT X\na[I][II]",
Program([], [
Designa(ID("a"), DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])])),
DesignaIndex(ID("a"), [Numeral("I"), Numeral("II")], Numeral("X")),
ExpressionStatement(ArrayIndex(ArrayIndex(ID("a"), Numeral("I")), Numeral("II"))),
]),
ValInt(10)),
# other elements unaffected
("DESIGNA a VT [[I, II], [III, IV]]\nDESIGNA a[I][II] VT X\na[II][I]",
Program([], [
Designa(ID("a"), DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])])),
DesignaIndex(ID("a"), [Numeral("I"), Numeral("II")], Numeral("X")),
ExpressionStatement(ArrayIndex(ArrayIndex(ID("a"), Numeral("II")), Numeral("I"))),
]),
ValInt(3)),
# dict inside array
('DESIGNA a VT [TABVLA {"x" VT I}]\nDESIGNA a[I]["x"] VT X\na[I]["x"]',
Program([], [
Designa(ID("a"), DataArray([DataDict([(String("x"), Numeral("I"))])])),
DesignaIndex(ID("a"), [Numeral("I"), String("x")], Numeral("X")),
ExpressionStatement(ArrayIndex(ArrayIndex(ID("a"), Numeral("I")), String("x"))),
]),
ValInt(10)),
# array inside dict
('DESIGNA d VT TABVLA {"a" VT [I, II]}\nDESIGNA d["a"][I] VT X\nd["a"][I]',
Program([], [
Designa(ID("d"), DataDict([(String("a"), DataArray([Numeral("I"), Numeral("II")]))])),
DesignaIndex(ID("d"), [String("a"), Numeral("I")], Numeral("X")),
ExpressionStatement(ArrayIndex(ArrayIndex(ID("d"), String("a")), Numeral("I"))),
]),
ValInt(10)),
# 3 levels deep
("DESIGNA a VT [[[I]]]\nDESIGNA a[I][I][I] VT X\na[I][I][I]",
Program([], [
Designa(ID("a"), DataArray([DataArray([DataArray([Numeral("I")])])])),
DesignaIndex(ID("a"), [Numeral("I"), Numeral("I"), Numeral("I")], Numeral("X")),
ExpressionStatement(ArrayIndex(ArrayIndex(ArrayIndex(ID("a"), Numeral("I")), Numeral("I")), Numeral("I"))),
]),
ValInt(10)),
]
class TestMultidimAssign(unittest.TestCase):
@parameterized.expand(multidim_assign_tests)
def test_multidim_assign(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Array slicing ---
array_slice_tests = [
# basic slice from middle
("[X, XX, XXX, XL, L][II VSQVE IV]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX"), Numeral("XL"), Numeral("L")]),
Numeral("II"), Numeral("IV")))]),
ValList([ValInt(20), ValInt(30), ValInt(40)])),
# slice of length 1
("[I, II, III][II VSQVE II]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III")]),
Numeral("II"), Numeral("II")))]),
ValList([ValInt(2)])),
# full array slice
("[I, II, III][I VSQVE III]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III")]),
Numeral("I"), Numeral("III")))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# slice on variable
("DESIGNA a VT [I, II, III, IV, V]\na[II VSQVE IV]",
Program([], [
Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")])),
ExpressionStatement(ArraySlice(ID("a"), Numeral("II"), Numeral("IV"))),
]),
ValList([ValInt(2), ValInt(3), ValInt(4)])),
# slice then index (chained)
("[I, II, III, IV][I VSQVE III][II]",
Program([], [ExpressionStatement(ArrayIndex(
ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV")]),
Numeral("I"), Numeral("III")),
Numeral("II")))]),
ValInt(2)),
# slice on range array
("[I VSQVE X][III VSQVE VII]",
Program([], [ExpressionStatement(ArraySlice(
DataRangeArray(Numeral("I"), Numeral("X")),
Numeral("III"), Numeral("VII")))]),
ValList([ValInt(3), ValInt(4), ValInt(5), ValInt(6), ValInt(7)])),
# expression as slice bounds
("[I, II, III, IV, V][I + I VSQVE II + II]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")]),
BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"),
BinOp(Numeral("II"), Numeral("II"), "SYMBOL_PLUS")))]),
ValList([ValInt(2), ValInt(3), ValInt(4)])),
]
class TestArraySlice(unittest.TestCase):
@parameterized.expand(array_slice_tests)
def test_array_slice(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Multiline arrays ---
multiline_array_tests = [
# newlines after commas
("[I,\nII,\nIII]",
Program([], [ExpressionStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# single newline after comma
("[I, II,\nIII]",
Program([], [ExpressionStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# empty array still works
("[]",
Program([], [ExpressionStatement(DataArray([]))]),
ValList([])),
# nested arrays with newlines
("[I,\n[II, III],\nIV]",
Program([], [ExpressionStatement(DataArray([
Numeral("I"),
DataArray([Numeral("II"), Numeral("III")]),
Numeral("IV")]))]),
ValList([ValInt(1), ValList([ValInt(2), ValInt(3)]), ValInt(4)])),
# empty array with newline
("[\n]",
Program([], [ExpressionStatement(DataArray([]))]),
ValList([])),
# newline immediately after [
("[\nI, II, III]",
Program([], [ExpressionStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# newline immediately before ]
("[I, II, III\n]",
Program([], [ExpressionStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# newlines on both sides
("[\nI, II, III\n]",
Program([], [ExpressionStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# newlines around every delimiter
("[\nI,\nII,\nIII\n]",
Program([], [ExpressionStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
]
class TestMultilineArray(unittest.TestCase):
@parameterized.expand(multiline_array_tests)
def test_multiline_array(self, source, nodes, value):
run_test(self, source, nodes, value)

222
tests/08_test_tabulas_.py Normal file
View File

@@ -0,0 +1,222 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Dict (TABVLA) ---
dict_tests = [
# empty dict
("TABVLA {}",
Program([], [ExpressionStatement(DataDict([]))]),
ValDict({})),
# single string key
('TABVLA {"a" VT I}',
Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I"))]))]),
ValDict({"a": ValInt(1)})),
# multiple entries
('TABVLA {"a" VT I, "b" VT II}',
Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]))]),
ValDict({"a": ValInt(1), "b": ValInt(2)})),
# integer keys
('TABVLA {I VT "one", II VT "two"}',
Program([], [ExpressionStatement(DataDict([(Numeral("I"), String("one")), (Numeral("II"), String("two"))]))]),
ValDict({1: ValStr("one"), 2: ValStr("two")})),
# expression values
('TABVLA {"x" VT I + II}',
Program([], [ExpressionStatement(DataDict([(String("x"), BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"))]))]),
ValDict({"x": ValInt(3)})),
# empty dict with newline
('TABVLA {\n}',
Program([], [ExpressionStatement(DataDict([]))]),
ValDict({})),
# single entry with surrounding newlines
('TABVLA {\n"a" VT I\n}',
Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I"))]))]),
ValDict({"a": ValInt(1)})),
# multiple entries with newlines after commas
('TABVLA {\n"a" VT I,\n"b" VT II\n}',
Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]))]),
ValDict({"a": ValInt(1), "b": ValInt(2)})),
# newlines around every delimiter
('TABVLA {\n"a" VT I,\n"b" VT II,\n"c" VT III\n}',
Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II")), (String("c"), Numeral("III"))]))]),
ValDict({"a": ValInt(1), "b": ValInt(2), "c": ValInt(3)})),
]
class TestDict(unittest.TestCase):
@parameterized.expand(dict_tests)
def test_dict(self, source, nodes, value):
run_test(self, source, nodes, value)
dict_index_tests = [
# string key access
('TABVLA {"a" VT X}["a"]',
Program([], [ExpressionStatement(ArrayIndex(DataDict([(String("a"), Numeral("X"))]), String("a")))]),
ValInt(10)),
# integer key access
('TABVLA {I VT "one"}[I]',
Program([], [ExpressionStatement(ArrayIndex(DataDict([(Numeral("I"), String("one"))]), Numeral("I")))]),
ValStr("one")),
# access via variable
('DESIGNA d VT TABVLA {"x" VT V}\nd["x"]',
Program([], [
Designa(ID("d"), DataDict([(String("x"), Numeral("V"))])),
ExpressionStatement(ArrayIndex(ID("d"), String("x"))),
]),
ValInt(5)),
# nested dict access
('TABVLA {"a" VT TABVLA {"b" VT X}}["a"]["b"]',
Program([], [ExpressionStatement(
ArrayIndex(ArrayIndex(DataDict([(String("a"), DataDict([(String("b"), Numeral("X"))]))]), String("a")), String("b"))
)]),
ValInt(10)),
]
class TestDictIndex(unittest.TestCase):
@parameterized.expand(dict_index_tests)
def test_dict_index(self, source, nodes, value):
run_test(self, source, nodes, value)
dict_assign_tests = [
# update existing key
('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["a"] VT X\nd["a"]',
Program([], [
Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])),
DesignaIndex(ID("d"), [String("a")], Numeral("X")),
ExpressionStatement(ArrayIndex(ID("d"), String("a"))),
]),
ValInt(10)),
# insert new key
('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["b"] VT II\nd["b"]',
Program([], [
Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])),
DesignaIndex(ID("d"), [String("b")], Numeral("II")),
ExpressionStatement(ArrayIndex(ID("d"), String("b"))),
]),
ValInt(2)),
# original key unaffected after insert
('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["b"] VT II\nd["a"]',
Program([], [
Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])),
DesignaIndex(ID("d"), [String("b")], Numeral("II")),
ExpressionStatement(ArrayIndex(ID("d"), String("a"))),
]),
ValInt(1)),
]
class TestDictAssign(unittest.TestCase):
@parameterized.expand(dict_assign_tests)
def test_dict_assign(self, source, nodes, value):
run_test(self, source, nodes, value)
dict_builtin_tests = [
# LONGITVDO on dict
('LONGITVDO(TABVLA {"a" VT I, "b" VT II})',
Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]),
ValInt(2)),
# LONGITVDO on empty dict
('LONGITVDO(TABVLA {})',
Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataDict([])]))]),
ValInt(0)),
# CLAVES
('CLAVES(TABVLA {"a" VT I, "b" VT II})',
Program([], [ExpressionStatement(BuiltIn("CLAVES", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]),
ValList([ValStr("a"), ValStr("b")])),
# CLAVES with int keys
('CLAVES(TABVLA {I VT "x", II VT "y"})',
Program([], [ExpressionStatement(BuiltIn("CLAVES", [DataDict([(Numeral("I"), String("x")), (Numeral("II"), String("y"))])]))]),
ValList([ValInt(1), ValInt(2)])),
]
class TestDictBuiltins(unittest.TestCase):
@parameterized.expand(dict_builtin_tests)
def test_dict_builtin(self, source, nodes, value):
run_test(self, source, nodes, value)
dict_iteration_tests = [
# PER iterates over keys
('DESIGNA r VT ""\nPER k IN TABVLA {"a" VT I, "b" VT II} FAC {\nDESIGNA r VT r & k\n}\nr',
Program([], [
Designa(ID("r"), String("")),
PerStatement(
DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]),
ID("k"),
[Designa(ID("r"), BinOp(ID("r"), ID("k"), "SYMBOL_AMPERSAND"))],
),
ExpressionStatement(ID("r")),
]),
ValStr("ab")),
]
class TestDictIteration(unittest.TestCase):
@parameterized.expand(dict_iteration_tests)
def test_dict_iteration(self, source, nodes, value):
run_test(self, source, nodes, value)
dict_display_tests = [
# DIC on dict
('DIC(TABVLA {"a" VT I})',
Program([], [ExpressionStatement(BuiltIn("DIC", [DataDict([(String("a"), Numeral("I"))])]))]),
ValStr("{a VT I}"), "{a VT I}\n"),
# DIC on multi-entry dict
('DIC(TABVLA {"a" VT I, "b" VT II})',
Program([], [ExpressionStatement(BuiltIn("DIC", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]),
ValStr("{a VT I, b VT II}"), "{a VT I, b VT II}\n"),
# DIC on empty dict
('DIC(TABVLA {})',
Program([], [ExpressionStatement(BuiltIn("DIC", [DataDict([])]))]),
ValStr("{}"), "{}\n"),
]
class TestDictDisplay(unittest.TestCase):
@parameterized.expand(dict_display_tests)
def test_dict_display(self, source, nodes, value, output):
run_test(self, source, nodes, value, output)
class TestDictGrowth(unittest.TestCase):
def test_dict_growth_preserves_order_and_lookup(self):
# Inserts XX entries via PER; pushes the compiled dict through
# multiple rehashes (initial cap=4) and verifies that lookup, length,
# and insertion-order iteration all still hold afterwards.
source = (
"DESIGNA d VT TABVLA {}\n"
"PER i IN [I VSQVE XX] FAC {\n"
"DESIGNA d[i] VT i * II\n"
"}\n"
"DIC(d[X])\n"
"DIC(LONGITVDO(d))\n"
"DIC(CLAVES(d))"
)
nodes = Program([], [
Designa(ID("d"), DataDict([])),
PerStatement(
DataRangeArray(Numeral("I"), Numeral("XX")),
ID("i"),
[DesignaIndex(ID("d"), [ID("i")],
BinOp(ID("i"), Numeral("II"), "SYMBOL_TIMES"))],
),
ExpressionStatement(BuiltIn("DIC", [ArrayIndex(ID("d"), Numeral("X"))])),
ExpressionStatement(BuiltIn("DIC", [BuiltIn("LONGITVDO", [ID("d")])])),
ExpressionStatement(BuiltIn("DIC", [BuiltIn("CLAVES", [ID("d")])])),
])
keys_str = "[" + " ".join(int_to_num(i, False) for i in range(1, 21)) + "]"
output = f"XX\nXX\n{keys_str}\n"
run_test(self, source, nodes, ValStr(keys_str), output)

263
tests/09_test_fraction.py Normal file
View File

@@ -0,0 +1,263 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- FRACTIO module ---
fractio_tests = [
# Basic fraction literals
("CVM FRACTIO\nIIIS",
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("IIIS"))]),
ValFrac(Fraction(7, 2))),
("CVM FRACTIO\nS",
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("S"))]),
ValFrac(Fraction(1, 2))),
("CVM FRACTIO\nS:.",
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("S:."))]),
ValFrac(Fraction(3, 4))),
("CVM FRACTIO\n.",
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("."))]),
ValFrac(Fraction(1, 12))),
("CVM FRACTIO\n:.",
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio(":."))]),
ValFrac(Fraction(1, 4))),
# Integer part with fraction
("CVM FRACTIO\nVIIS:|::",
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("VIIS:|::"))]),
ValFrac(Fraction(7) + Fraction(100, 144))),
# Arithmetic
("CVM FRACTIO\nIIIS + S",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("S"), "SYMBOL_PLUS"))
]),
ValFrac(Fraction(4))
),
("CVM FRACTIO\nIIIS - S",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("S"), "SYMBOL_MINUS"))
]),
ValFrac(Fraction(3))
),
("CVM FRACTIO\nS * IV",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("S"), Numeral("IV"), "SYMBOL_TIMES"))
]),
ValFrac(Fraction(2))
),
# Division returns fraction
("CVM FRACTIO\nI / IV",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Numeral("I"), Numeral("IV"), "SYMBOL_DIVIDE"))
]),
ValFrac(Fraction(1, 4))
),
("CVM FRACTIO\nI / III",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Numeral("I"), Numeral("III"), "SYMBOL_DIVIDE"))
]),
ValFrac(Fraction(1, 3))
),
# Integer division still works without fractions in operands... but with FRACTIO returns ValFrac
("CVM FRACTIO\nX / II",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"))
]),
ValFrac(Fraction(5))
),
# Modulo on fractions: 7/2 RELIQVVM 3/2 = 1/2 (7/2 / 3/2 = 7/3, floor=2, 7/2 - 3 = 1/2)
("CVM FRACTIO\nIIIS RELIQVVM IS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IS"), "KEYWORD_RELIQVVM"))
]),
ValFrac(Fraction(1, 2))
),
# Modulo with mixed operand types: 5/2 RELIQVVM 1 = 1/2
("CVM FRACTIO\nIIS RELIQVVM I",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIS"), Numeral("I"), "KEYWORD_RELIQVVM"))
]),
ValFrac(Fraction(1, 2))
),
# Int operands under FRACTIO still return a fraction: 10 RELIQVVM 3 = 1 (as Fraction)
("CVM FRACTIO\nX RELIQVVM III",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "KEYWORD_RELIQVVM"))
]),
ValFrac(Fraction(1))
),
# Exact multiple under FRACTIO: 3 RELIQVVM 3/2 = 0
("CVM FRACTIO\nIII RELIQVVM IS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Numeral("III"), Fractio("IS"), "KEYWORD_RELIQVVM"))
]),
ValFrac(Fraction(0))
),
# String concatenation with fraction
("CVM FRACTIO\nDIC(IIIS & \"!\")",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BuiltIn("DIC", [BinOp(Fractio("IIIS"), String("!"), "SYMBOL_AMPERSAND")]))
]),
ValStr("IIIS!"), "IIIS!\n"
),
# Negative fractions
("CVM FRACTIO\nCVM SVBNVLLA\n-IIS",
Program([ModuleCall("FRACTIO"),ModuleCall("SVBNVLLA")],[
ExpressionStatement(UnaryMinus(Fractio("IIS")))
]),
ValFrac(Fraction(-5,2))
)
]
class TestFractio(unittest.TestCase):
@parameterized.expand(fractio_tests)
def test_fractio(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
fractio_comparison_tests = [
# fraction vs fraction
("CVM FRACTIO\nIIIS PLVS III",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Numeral("III"), "KEYWORD_PLVS"))
]),
ValBool(True)
),
("CVM FRACTIO\nIII MINVS IIIS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Numeral("III"), Fractio("IIIS"), "KEYWORD_MINVS"))
]),
ValBool(True)
),
("CVM FRACTIO\nIIIS MINVS IV",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Numeral("IV"), "KEYWORD_MINVS"))
]),
ValBool(True)
),
("CVM FRACTIO\nIV PLVS IIIS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Numeral("IV"), Fractio("IIIS"), "KEYWORD_PLVS"))
]),
ValBool(True)
),
("CVM FRACTIO\nIIIS PLVS IIIS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_PLVS"))
]),
ValBool(False)
),
("CVM FRACTIO\nIIIS MINVS IIIS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_MINVS"))
]),
ValBool(False)
),
# HAVD_PLVS / HAVD_MINVS on fractions — equality boundary distinguishes from MINVS / PLVS
("CVM FRACTIO\nIIIS HAVD_PLVS III",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Numeral("III"), "KEYWORD_HAVD_PLVS"))
]),
ValBool(False) # 3.5 <= 3 is false
),
("CVM FRACTIO\nIIIS HAVD_MINVS IIIS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_HAVD_MINVS"))
]),
ValBool(True) # 3.5 >= 3.5 is true (equality boundary)
),
("CVM FRACTIO\nIIIS HAVD_PLVS IIIS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_HAVD_PLVS"))
]),
ValBool(True) # 3.5 <= 3.5 is true (equality boundary)
),
# equality: fraction == fraction
("CVM FRACTIO\nIIIS EST IIIS",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_EST"))
]),
ValBool(True)
),
("CVM FRACTIO\nIIIS EST IV",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(Fractio("IIIS"), Numeral("IV"), "KEYWORD_EST"))
]),
ValBool(False)
),
# equality: fraction == whole number (ValFrac(4) vs ValInt(4))
("CVM FRACTIO\nIIIS + S EST IV",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(
BinOp(Fractio("IIIS"), Fractio("S"), "SYMBOL_PLUS"),
Numeral("IV"), "KEYWORD_EST"))
]),
ValBool(True)
),
("CVM FRACTIO\nS + S EST I",
Program([ModuleCall("FRACTIO")], [
ExpressionStatement(BinOp(
BinOp(Fractio("S"), Fractio("S"), "SYMBOL_PLUS"),
Numeral("I"), "KEYWORD_EST"))
]),
ValBool(True)
),
]
class TestFractioComparisons(unittest.TestCase):
@parameterized.expand(fractio_comparison_tests)
def test_fractio_comparison(self, source, nodes, value):
run_test(self, source, nodes, value)
class TestFractioHelpers(unittest.TestCase):
def test_frac_to_fraction_ordering(self):
with self.assertRaises(CentvrionError):
frac_to_fraction(".S") # . before S violates highest-to-lowest
def test_frac_to_fraction_level_overflow(self):
with self.assertRaises(CentvrionError):
frac_to_fraction("SSSSSS") # SS means S twice = 12/12 = 1, violating < 12/12 constraint
def test_frac_to_fraction_iiis(self):
self.assertEqual(frac_to_fraction("IIIS"), Fraction(7, 2))
def test_frac_to_fraction_s_colon_dot(self):
self.assertEqual(frac_to_fraction("S:."), Fraction(3, 4))
def test_frac_to_fraction_dot(self):
self.assertEqual(frac_to_fraction("."), Fraction(1, 12))
def test_frac_to_fraction_multilevel(self):
self.assertEqual(frac_to_fraction("VIIS:|::"), Fraction(7) + Fraction(100, 144))
def test_fraction_to_frac_iiis(self):
self.assertEqual(fraction_to_frac(Fraction(7, 2)), "IIIS")
def test_fraction_to_frac_s_colon_dot(self):
self.assertEqual(fraction_to_frac(Fraction(3, 4)), "S:.")
def test_fraction_to_frac_dot(self):
self.assertEqual(fraction_to_frac(Fraction(1, 12)), ".")
def test_fraction_to_frac_multilevel(self):
self.assertEqual(
fraction_to_frac(Fraction(7) + Fraction(100, 144)),
"VIIS:|::"
)
def test_roundtrip(self):
# Only canonical forms roundtrip — fraction_to_frac always uses max colons before dots
for s in ["IIIS", "S:.", ".", "::", "VIIS:|::", "S"]:
self.assertEqual(fraction_to_frac(frac_to_fraction(s)), s)

406
tests/10_test_external.py Normal file
View File

@@ -0,0 +1,406 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- DORMI ---
dormi_tests = [
("DORMI(NVLLVS)",
Program([], [ExpressionStatement(BuiltIn("DORMI", [Nullus()]))]),
ValNul()),
]
class TestDormi(unittest.TestCase):
@parameterized.expand(dormi_tests)
def test_dormi(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
def test_dormi_timing_int(self):
source = "DORMI(I)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
start = time.time()
program.eval()
elapsed = time.time() - start
self.assertAlmostEqual(elapsed, 1.0, delta=0.5)
def test_dormi_timing_int_compiled(self):
source = "DORMI(I)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
c_source = compile_program(program)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
start = time.time()
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
elapsed = time.time() - start
self.assertEqual(proc.returncode, 0)
self.assertAlmostEqual(elapsed, 1.0, delta=0.5)
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
def test_dormi_timing_frac(self):
source = "CVM FRACTIO\nDORMI(S)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
start = time.time()
program.eval()
elapsed = time.time() - start
self.assertAlmostEqual(elapsed, 0.5, delta=0.5)
def test_dormi_timing_frac_compiled(self):
source = "CVM FRACTIO\nDORMI(S)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
c_source = compile_program(program)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
start = time.time()
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
elapsed = time.time() - start
self.assertEqual(proc.returncode, 0)
self.assertAlmostEqual(elapsed, 0.5, delta=0.5)
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
class TestScripta(unittest.TestCase):
def _run_scripta(self, source):
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
program = Parser().parse(tokens)
# printer round-trip
new_text = program.print()
new_tokens = Lexer().get_lexer().lex(new_text + "\n")
new_nodes = Parser().parse(new_tokens)
self.assertEqual(program, new_nodes, f"Printer test\n{source}\n{new_text}")
return program
def _compile_and_run(self, program):
c_source = compile_program(program)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
self.assertEqual(proc.returncode, 0, f"Compiler binary exited non-zero:\n{proc.stderr}")
return proc.stdout
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
def test_scribe_and_lege(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDIC(LEGE("{path}"))'
program = self._run_scripta(source)
captured = StringIO()
with patch("sys.stdout", captured):
program.eval()
self.assertEqual(captured.getvalue(), "SALVE MVNDE\n")
with open(path) as f:
self.assertEqual(f.read(), "SALVE MVNDE")
finally:
if os.path.exists(path):
os.unlink(path)
def test_scribe_and_lege_compiled(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDIC(LEGE("{path}"))'
program = self._run_scripta(source)
output = self._compile_and_run(program)
self.assertEqual(output, "SALVE MVNDE\n")
with open(path) as f:
self.assertEqual(f.read(), "SALVE MVNDE")
finally:
if os.path.exists(path):
os.unlink(path)
def test_adivnge(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDIC(LEGE("{path}"))'
program = self._run_scripta(source)
captured = StringIO()
with patch("sys.stdout", captured):
program.eval()
self.assertEqual(captured.getvalue(), "SALVE MVNDE\n")
finally:
if os.path.exists(path):
os.unlink(path)
def test_adivnge_compiled(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDIC(LEGE("{path}"))'
program = self._run_scripta(source)
output = self._compile_and_run(program)
self.assertEqual(output, "SALVE MVNDE\n")
finally:
if os.path.exists(path):
os.unlink(path)
def test_scribe_overwrites(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "first")\nSCRIBE("{path}", "second")\nLEGE("{path}")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValStr("second"))
finally:
if os.path.exists(path):
os.unlink(path)
def test_lege_empty_file(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
path = f.name
try:
source = f'CVM SCRIPTA\nLEGE("{path}")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValStr(""))
finally:
os.unlink(path)
def test_lege_preexisting_content(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
f.write("hello from python")
path = f.name
try:
source = f'CVM SCRIPTA\nLEGE("{path}")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValStr("hello from python"))
finally:
os.unlink(path)
def test_scribe_returns_nullus(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "x")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValNul())
finally:
if os.path.exists(path):
os.unlink(path)
def test_adivnge_returns_nullus(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nADIVNGE("{path}", "x")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValNul())
finally:
if os.path.exists(path):
os.unlink(path)
class TestRete(unittest.TestCase):
@classmethod
def setUpClass(cls):
import http.server, threading
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"SALVE MVNDE")
def log_message(self, *args):
pass
cls.server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
cls.port = cls.server.server_address[1]
cls.thread = threading.Thread(target=cls.server.serve_forever)
cls.thread.daemon = True
cls.thread.start()
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
def test_pete(self):
url = f"http://127.0.0.1:{self.port}/"
source = f'CVM RETE\nPETE("{url}")'
run_test(self, source,
Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("PETE", [String(url)]))]),
ValStr("SALVE MVNDE"))
def test_pete_dic(self):
url = f"http://127.0.0.1:{self.port}/"
source = f'CVM RETE\nDIC(PETE("{url}"))'
run_test(self, source,
Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("PETE", [String(url)])]))]),
ValStr("SALVE MVNDE"), "SALVE MVNDE\n")
class TestReteServer(unittest.TestCase):
"""Integration tests for PETITVR + AVSCVLTA server functionality."""
def _wait_for_server(self, port, timeout=2.0):
"""Poll until the server is accepting connections."""
import socket
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection(("127.0.0.1", port), timeout=0.1):
return
except OSError:
time.sleep(0.05)
self.fail(f"Server on port {port} did not start within {timeout}s")
def _free_port(self):
"""Find a free port in range 1024-3999 (representable without MAGNVM)."""
import socket, random
for _ in range(100):
port = random.randint(1024, 3999)
try:
with socket.socket() as s:
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
raise RuntimeError("Could not find a free port in range 1024-3999")
def _run_server(self, source):
"""Parse and eval source in a daemon thread. Returns when server is ready."""
import threading
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
program = Parser().parse(tokens)
t = threading.Thread(target=program.eval, daemon=True)
t.start()
def test_basic_handler(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/", FVNCTIO (petitio) VT {{\n'
f'REDI("SALVE MVNDE")\n'
f'}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/")
self.assertEqual(resp.read().decode(), "SALVE MVNDE")
def test_multiple_routes(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/", FVNCTIO (p) VT {{\nREDI("RADIX")\n}})\n'
f'PETITVR("/nomen", FVNCTIO (p) VT {{\nREDI("MARCVS")\n}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp1 = urllib.request.urlopen(f"http://127.0.0.1:{port}/")
self.assertEqual(resp1.read().decode(), "RADIX")
resp2 = urllib.request.urlopen(f"http://127.0.0.1:{port}/nomen")
self.assertEqual(resp2.read().decode(), "MARCVS")
def test_404_unmatched(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/", FVNCTIO (p) VT {{\nREDI("ok")\n}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request, urllib.error
with self.assertRaises(urllib.error.HTTPError) as ctx:
urllib.request.urlopen(f"http://127.0.0.1:{port}/nonexistent")
self.assertEqual(ctx.exception.code, 404)
def test_request_dict_via(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/echo", FVNCTIO (petitio) VT {{\n'
f'REDI(petitio["via"])\n'
f'}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/echo")
self.assertEqual(resp.read().decode(), "/echo")
def test_request_dict_quaestio(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/q", FVNCTIO (petitio) VT {{\n'
f'REDI(petitio["quaestio"])\n'
f'}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/q?nomen=Marcus")
self.assertEqual(resp.read().decode(), "nomen=Marcus")
def test_petitvr_stores_route(self):
"""PETITVR alone (without AVSCVLTA) just stores a route and returns NVLLVS."""
source = 'CVM RETE\nPETITVR("/", FVNCTIO (p) VT {\nREDI("hi")\n})'
run_test(self, source,
Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("PETITVR", [
String("/"),
Fvnctio([ID("p")], [Redi([String("hi")])])
]))]),
ValNul())

443
tests/11_test_assorted.py Normal file
View File

@@ -0,0 +1,443 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Comments ---
comment_tests = [
# trailing line comment
('DIC("hello") // this is ignored', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello")]))]), ValStr("hello"), "hello\n"),
# line comment on its own line before code
('// ignored\nDIC("hi")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hi")]))]), ValStr("hi"), "hi\n"),
# inline block comment
('DIC(/* ignored */ "hi")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hi")]))]), ValStr("hi"), "hi\n"),
# block comment spanning multiple lines
('/* line one\nline two */\nDIC("hi")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hi")]))]), ValStr("hi"), "hi\n"),
# block comment mid-expression
("II /* ignored */ + III", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"))]), ValInt(5)),
# line comment after expression (no output)
("II + III // ignored", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"))]), ValInt(5)),
# division still works (/ token not confused with //)
("X / II", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"))]), ValInt(5)),
# multiple line comments
('// first\n// second\nDIC("ok")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("ok")]))]), ValStr("ok"), "ok\n"),
# comment-only line between two statements
('DESIGNA x VT I\n// set y\nDESIGNA y VT II\nx + y',
Program([], [Designa(ID("x"), Numeral("I")), Designa(ID("y"), Numeral("II")), ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS"))]),
ValInt(3)),
# blank line between two statements (double newline)
('DESIGNA x VT I\n\nDESIGNA y VT II\nx + y',
Program([], [Designa(ID("x"), Numeral("I")), Designa(ID("y"), Numeral("II")), ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS"))]),
ValInt(3)),
# multiple comment-only lines between statements
('DESIGNA x VT I\n// one\n// two\nDESIGNA y VT III\nx + y',
Program([], [Designa(ID("x"), Numeral("I")), Designa(ID("y"), Numeral("III")), ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS"))]),
ValInt(4)),
]
class TestComments(unittest.TestCase):
@parameterized.expand(comment_tests)
def test_comments(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- Scope ---
scope_tests = [
# SI: variable assigned in true branch persists in outer scope
("SI VERITAS TVNC { DESIGNA r VT X }\nr",
Program([], [SiStatement(Bool(True), [Designa(ID("r"), Numeral("X"))], None), ExpressionStatement(ID("r"))]),
ValInt(10)),
# SI: variable assigned in ALIVD branch persists in outer scope
("SI FALSITAS TVNC { DESIGNA r VT X } ALIVD { DESIGNA r VT V }\nr",
Program([], [SiStatement(Bool(False), [Designa(ID("r"), Numeral("X"))], [Designa(ID("r"), Numeral("V"))]), ExpressionStatement(ID("r"))]),
ValInt(5)),
# DVM: variable assigned in body persists after loop exits
# x goes 1→2→3→4→5; r tracks x each iteration; loop exits when x==5
("DESIGNA x VT I\nDVM x EST V FAC { DESIGNA x VT x + I\nDESIGNA r VT x }\nr",
Program([], [
Designa(ID("x"), Numeral("I")),
DumStatement(BinOp(ID("x"), Numeral("V"), "KEYWORD_EST"), [
Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")),
Designa(ID("r"), ID("x")),
]),
ExpressionStatement(ID("r")),
]),
ValInt(5)),
# PER: loop variable holds last array element after loop (no ERVMPE)
("PER i IN [I, II, III] FAC { DESIGNA nop VT I }\ni",
Program([], [
PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [Designa(ID("nop"), Numeral("I"))]),
ExpressionStatement(ID("i")),
]),
ValInt(3)),
# PER: reassigning loop var in body doesn't prevent remaining iterations from running
# cnt increments once per iteration (all 3); C=100 doesn't replace next element assignment
("DESIGNA cnt VT I\nPER i IN [I, II, III] FAC { DESIGNA i VT C\nDESIGNA cnt VT cnt + I }\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [
Designa(ID("i"), Numeral("C")),
Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS")),
]),
ExpressionStatement(ID("cnt")),
]),
ValInt(4)),
# PER: loop var after loop reflects the last body assignment, not the last array element
# body sets i=C=100 on every iteration; after loop ends, i stays at 100
("PER i IN [I, II, III] FAC { DESIGNA i VT C }\ni",
Program([], [
PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [Designa(ID("i"), Numeral("C"))]),
ExpressionStatement(ID("i")),
]),
ValInt(100)),
# DONICVM: counter holds last range value after loop ends
# [I VSQVE IV] = [1,2,3,4]; last value assigned by loop is IV=4
("DONICVM i VT I VSQVE IV FAC { DESIGNA nop VT I }\ni",
Program([], [
PerStatement(DataRangeArray(Numeral("I"), Numeral("IV")), ID("i"), [Designa(ID("nop"), Numeral("I"))]),
ExpressionStatement(ID("i")),
]),
ValInt(4)),
# DONICVM: reassigning counter inside body doesn't reduce the number of iterations
# range [I VSQVE IV] evaluated once; i reset each time; cnt still increments 4 times → 5
("DESIGNA cnt VT I\nDONICVM i VT I VSQVE IV FAC { DESIGNA cnt VT cnt + I\nDESIGNA i VT C }\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
PerStatement(DataRangeArray(Numeral("I"), Numeral("IV")), ID("i"), [
Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS")),
Designa(ID("i"), Numeral("C")),
]),
ExpressionStatement(ID("cnt")),
]),
ValInt(5)),
# DONICVM: ERVMPE exits loop early; counter persists at break value
("DONICVM i VT I VSQVE X FAC {\nSI i EST III TVNC { ERVMPE }\n}\ni",
Program([], [
PerStatement(DataRangeArray(Numeral("I"), Numeral("X")), ID("i"), [
SiStatement(BinOp(ID("i"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None),
]),
ExpressionStatement(ID("i")),
]),
ValInt(3)),
# Function: modifying parameter inside function does not affect outer variable of same name
# outer n=1; f receives n=5 and modifies its local copy; outer n unchanged
("DESIGNA n VT I\nDEFINI f (n) VT { DESIGNA n VT n + X\nREDI (n) }\nINVOCA f (V)\nn",
Program([], [
Designa(ID("n"), Numeral("I")),
Defini(ID("f"), [ID("n")], [Designa(ID("n"), BinOp(ID("n"), Numeral("X"), "SYMBOL_PLUS")), Redi([ID("n")])]),
ExpressionStatement(Invoca(ID("f"), [Numeral("V")])),
ExpressionStatement(ID("n")),
]),
ValInt(1)),
# Function: mutating outer variable inside function (via DESIGNA) is not visible outside
# Invoca creates func_vtable = vtable.copy(); mutations to func_vtable don't propagate back
("DESIGNA x VT I\nDEFINI f () VT { DESIGNA x VT C\nREDI (x) }\nINVOCA f ()\nx",
Program([], [
Designa(ID("x"), Numeral("I")),
Defini(ID("f"), [], [Designa(ID("x"), Numeral("C")), Redi([ID("x")])]),
ExpressionStatement(Invoca(ID("f"), [])),
ExpressionStatement(ID("x")),
]),
ValInt(1)),
# Function: two successive calls with same parameter name don't share state
("DEFINI f (n) VT { REDI (n * II) }\nINVOCA f (III) + INVOCA f (IV)",
Program([], [
Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]),
ExpressionStatement(BinOp(Invoca(ID("f"), [Numeral("III")]), Invoca(ID("f"), [Numeral("IV")]), "SYMBOL_PLUS")),
]),
ValInt(14)),
# Function: calling f(I) with param named n does not overwrite outer n=II
# f is defined before n is designated; INVOCA creates a local copy, outer vtable unchanged
("DEFINI f (n) VT { REDI (n * II) }\nDESIGNA n VT II\nINVOCA f (I)\nn",
Program([], [
Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]),
Designa(ID("n"), Numeral("II")),
ExpressionStatement(Invoca(ID("f"), [Numeral("I")])),
ExpressionStatement(ID("n")),
]),
ValInt(2)),
]
class TestScope(unittest.TestCase):
@parameterized.expand(scope_tests)
def test_scope(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- First-class functions / FVNCTIO ---
fvnctio_tests = [
# Lambda assigned to variable, then called
(
"DESIGNA f VT FVNCTIO (x) VT { REDI (x + I) }\nINVOCA f (V)",
Program([], [
Designa(ID("f"), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])])),
ExpressionStatement(Invoca(ID("f"), [Numeral("V")])),
]),
ValInt(6),
),
# IIFE: immediately invoked lambda
(
"INVOCA FVNCTIO (x) VT { REDI (x * II) } (III)",
Program([], [
ExpressionStatement(Invoca(
Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("II"), "SYMBOL_TIMES")])]),
[Numeral("III")],
)),
]),
ValInt(6),
),
# Zero-arg lambda
(
"INVOCA FVNCTIO () VT { REDI (XLII) } ()",
Program([], [
ExpressionStatement(Invoca(
Fvnctio([], [Redi([Numeral("XLII")])]),
[],
)),
]),
ValInt(42),
),
# Function passed as argument
(
"DEFINI apply (f, x) VT { REDI (INVOCA f (x)) }\n"
"DESIGNA dbl VT FVNCTIO (n) VT { REDI (n * II) }\n"
"INVOCA apply (dbl, V)",
Program([], [
Defini(ID("apply"), [ID("f"), ID("x")], [
Redi([Invoca(ID("f"), [ID("x")])])
]),
Designa(ID("dbl"), Fvnctio([ID("n")], [
Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])
])),
ExpressionStatement(Invoca(ID("apply"), [ID("dbl"), Numeral("V")])),
]),
ValInt(10),
),
# Lambda uses caller-scope variable (copy-caller semantics)
(
"DESIGNA n VT III\n"
"DESIGNA f VT FVNCTIO (x) VT { REDI (x + n) }\n"
"INVOCA f (V)",
Program([], [
Designa(ID("n"), Numeral("III")),
Designa(ID("f"), Fvnctio([ID("x")], [
Redi([BinOp(ID("x"), ID("n"), "SYMBOL_PLUS")])
])),
ExpressionStatement(Invoca(ID("f"), [Numeral("V")])),
]),
ValInt(8),
),
# Named function passed as value
(
"DEFINI sqr (x) VT { REDI (x * x) }\n"
"DESIGNA f VT sqr\n"
"INVOCA f (IV)",
Program([], [
Defini(ID("sqr"), [ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_TIMES")])]),
Designa(ID("f"), ID("sqr")),
ExpressionStatement(Invoca(ID("f"), [Numeral("IV")])),
]),
ValInt(16),
),
# Nested lambdas
(
"INVOCA FVNCTIO (x) VT { REDI (INVOCA FVNCTIO (y) VT { REDI (y + I) } (x)) } (V)",
Program([], [
ExpressionStatement(Invoca(
Fvnctio([ID("x")], [
Redi([Invoca(
Fvnctio([ID("y")], [Redi([BinOp(ID("y"), Numeral("I"), "SYMBOL_PLUS")])]),
[ID("x")],
)])
]),
[Numeral("V")],
)),
]),
ValInt(6),
),
# DIC on a function value
(
"DESIGNA f VT FVNCTIO (x) VT { REDI (x) }\nDIC(f)",
Program([], [
Designa(ID("f"), Fvnctio([ID("x")], [Redi([ID("x")])])),
ExpressionStatement(BuiltIn("DIC", [ID("f")])),
]),
ValStr("FVNCTIO"),
"FVNCTIO\n",
),
# Lambda stored in array, called via index
(
"DESIGNA fns VT [FVNCTIO (x) VT { REDI (x + I) }, FVNCTIO (x) VT { REDI (x * II) }]\n"
"INVOCA fns[I] (V)",
Program([], [
Designa(ID("fns"), DataArray([
Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])]),
Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("II"), "SYMBOL_TIMES")])]),
])),
ExpressionStatement(Invoca(
ArrayIndex(ID("fns"), Numeral("I")),
[Numeral("V")],
)),
]),
ValInt(6),
),
# Lambda stored in dict, called via key
(
'DESIGNA d VT TABVLA {"add" VT FVNCTIO (x) VT { REDI (x + I) }}\n'
'INVOCA d["add"] (V)',
Program([], [
Designa(ID("d"), DataDict([
(String("add"), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])])),
])),
ExpressionStatement(Invoca(
ArrayIndex(ID("d"), String("add")),
[Numeral("V")],
)),
]),
ValInt(6),
),
# Multi-param lambda
(
"DESIGNA add VT FVNCTIO (a, b) VT { REDI (a + b) }\nINVOCA add (III, IV)",
Program([], [
Designa(ID("add"), Fvnctio([ID("a"), ID("b")], [
Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])
])),
ExpressionStatement(Invoca(ID("add"), [Numeral("III"), Numeral("IV")])),
]),
ValInt(7),
),
]
class TestFvnctio(unittest.TestCase):
@parameterized.expand(fvnctio_tests)
def test_fvnctio(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- Tempta/Cape (try/catch) ---
tempta_tests = [
# Try block succeeds — catch not entered
(
"TEMPTA {\nDESIGNA r VT I\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
TemptaStatement(
[Designa(ID("r"), Numeral("I"))],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(1),
),
# Try block errors — caught by catch
(
"TEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
TemptaStatement(
[Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(2),
),
# Error variable contains the error message
(
'DESIGNA e VT NVLLVS\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nNVLLVS\n}\ne',
Program([], [
Designa(ID("e"), Nullus()),
TemptaStatement(
[Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[ExpressionStatement(Nullus())],
),
ExpressionStatement(ID("e")),
]),
ValStr("Division by zero"),
),
# Nested tempta — inner catches, outer unaffected
(
"DESIGNA r VT NVLLVS\nTEMPTA {\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\n}\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
Designa(ID("r"), Nullus()),
TemptaStatement(
[TemptaStatement(
[Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Designa(ID("r"), Numeral("I"))],
)],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(1),
),
# REDI inside catch block
(
"DEFINI f () VT {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nREDI (III)\n}\nREDI (IV)\n}\nINVOCA f ()",
Program([], [
Defini(ID("f"), [], [
TemptaStatement(
[Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Redi([Numeral("III")])],
),
Redi([Numeral("IV")]),
]),
ExpressionStatement(Invoca(ID("f"), [])),
]),
ValInt(3),
),
# ERVMPE inside catch block (inside a loop)
(
"DESIGNA r VT NVLLVS\nDVM r EST I FAC {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\nERVMPE\n}\n}\nr",
Program([], [
Designa(ID("r"), Nullus()),
DumStatement(
BinOp(ID("r"), Numeral("I"), "KEYWORD_EST"),
[TemptaStatement(
[Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Designa(ID("r"), Numeral("I")), Erumpe()],
)],
),
ExpressionStatement(ID("r")),
]),
ValInt(1),
),
# Statement after error in try block is not executed
(
"DESIGNA r VT NVLLVS\nTEMPTA {\nDESIGNA x VT I / NVLLVS\nDESIGNA r VT III\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
Designa(ID("r"), Nullus()),
TemptaStatement(
[Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE")),
Designa(ID("r"), Numeral("III"))],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(2),
),
]
class TestTempta(unittest.TestCase):
@parameterized.expand(tempta_tests)
def test_tempta(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)

173
tests/12_test_iason___.py Normal file
View File

@@ -0,0 +1,173 @@
from tests._helpers import (
unittest, parameterized, Fraction,
run_test,
Bool, BuiltIn, DataArray, DataDict, Designa, ExpressionStatement, ID,
ModuleCall, Nullus, Numeral, Program, String,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFrac,
)
def _scribe(arg, modules=("IASON",)):
return Program(
[ModuleCall(m) for m in modules],
[ExpressionStatement(BuiltIn("IASON_SCRIBE", [arg]))],
)
def _lege(arg, modules=("IASON",)):
return Program(
[ModuleCall(m) for m in modules],
[ExpressionStatement(BuiltIn("IASON_LEGE", [String(arg)]))],
)
def _src_lege(arg, extra_modules=()):
modules = ("IASON",) + tuple(extra_modules)
prefix = "\n".join(f"CVM {m}" for m in modules) + "\n"
return prefix + f"IASON_LEGE('{arg}')"
def _src_scribe(arg_text, extra_modules=()):
modules = ("IASON",) + tuple(extra_modules)
prefix = "\n".join(f"CVM {m}" for m in modules) + "\n"
return prefix + f"IASON_SCRIBE({arg_text})"
iason_tests = [
# ---- Parse: scalars ----
(_src_lege("null"), _lege("null"), ValNul()),
(_src_lege("true"), _lege("true"), ValBool(True)),
(_src_lege("false"), _lege("false"), ValBool(False)),
(_src_lege("42"), _lege("42"), ValInt(42)),
(_src_lege('"hello"'), _lege('"hello"'), ValStr("hello")),
# ---- Parse: empty containers ----
(_src_lege("[]"), _lege("[]"), ValList([])),
(_src_lege("{}"), _lege("{}"), ValDict({})),
# ---- Parse: array of mixed types ----
(_src_lege('[1, true, null, "x"]'),
_lege('[1, true, null, "x"]'),
ValList([ValInt(1), ValBool(True), ValNul(), ValStr("x")])),
# ---- Parse: nested ----
(_src_lege('{"a": [1, 2], "b": {"c": 3}}'),
_lege('{"a": [1, 2], "b": {"c": 3}}'),
ValDict({
"a": ValList([ValInt(1), ValInt(2)]),
"b": ValDict({"c": ValInt(3)}),
})),
# ---- Parse: numbers ----
(_src_lege("-7"), _lege("-7"), ValInt(-7)),
(_src_lege("0"), _lege("0"), ValInt(0)),
# ---- Parse: string escapes ----
# NB: single-quoted CENTVRION strings unescape \n / \" / \\ before the
# JSON parser sees them, so direct parse tests for those escapes would
# have ambiguous semantics. Serialize tests below cover the inverse, and
# this \u test exercises the JSON parser's escape path.
(_src_lege('"\\u00e9"'),
_lege('"\\u00e9"'),
ValStr("é")),
# ---- Parse: float without FRACTIO floors ----
(_src_lege("3.7"), _lege("3.7"), ValInt(3)),
(_src_lege("-2.5"), _lege("-2.5"), ValInt(-3)),
(_src_lege("1e2"), _lege("1e2"), ValInt(100)),
# ---- Parse: float with FRACTIO is exact ----
(_src_lege("0.5", extra_modules=("FRACTIO",)),
_lege("0.5", modules=("IASON", "FRACTIO")),
ValFrac(Fraction(1, 2))),
(_src_lege("0.1", extra_modules=("FRACTIO",)),
_lege("0.1", modules=("IASON", "FRACTIO")),
ValFrac(Fraction(1, 10))),
(_src_lege("-0.25", extra_modules=("FRACTIO",)),
_lege("-0.25", modules=("IASON", "FRACTIO")),
ValFrac(Fraction(-1, 4))),
(_src_lege("5", extra_modules=("FRACTIO",)),
_lege("5", modules=("IASON", "FRACTIO")),
ValInt(5)),
(_src_lege("3.0", extra_modules=("FRACTIO",)),
_lege("3.0", modules=("IASON", "FRACTIO")),
ValInt(3)),
# ---- Serialize: scalars ----
(_src_scribe("NVLLVS"), _scribe(Nullus()), ValStr("null")),
(_src_scribe("VERITAS"), _scribe(Bool(True)), ValStr("true")),
(_src_scribe("FALSITAS"), _scribe(Bool(False)), ValStr("false")),
(_src_scribe("XLII"), _scribe(Numeral("XLII")), ValStr("42")),
(_src_scribe('"hello"'), _scribe(String("hello")), ValStr('"hello"')),
(_src_scribe("[]"), _scribe(DataArray([])), ValStr("[]")),
(_src_scribe("TABVLA {}"), _scribe(DataDict([])), ValStr("{}")),
# ---- Serialize: nested ----
(_src_scribe("[I, II, III]"),
_scribe(DataArray([Numeral("I"), Numeral("II"), Numeral("III")])),
ValStr("[1, 2, 3]")),
(_src_scribe('TABVLA {"a" VT I, "b" VT VERITAS}'),
_scribe(DataDict([(String("a"), Numeral("I")), (String("b"), Bool(True))])),
ValStr('{"a": 1, "b": true}')),
# ---- Serialize: special chars ----
(_src_scribe('"a\\nb"'),
_scribe(String("a\nb")),
ValStr('"a\\nb"')),
(_src_scribe('"a\\"b"'),
_scribe(String('a"b')),
ValStr('"a\\"b"')),
(_src_scribe('"a\\\\b"'),
_scribe(String("a\\b")),
ValStr('"a\\\\b"')),
# ---- Round-trip ----
("CVM IASON\nDIC(IASON_LEGE('[1, 2, 3]'))",
Program([ModuleCall("IASON")], [ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_LEGE", [String("[1, 2, 3]")])]))]),
ValStr("[I II III]"), "[I II III]\n"),
("CVM IASON\nDIC(IASON_SCRIBE(IASON_LEGE('{\"a\": [1, true, null]}')))",
Program([ModuleCall("IASON")], [ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE",
[BuiltIn("IASON_LEGE", [String('{"a": [1, true, null]}')])])]))]),
ValStr('{"a": [1, true, null]}'),
'{"a": [1, true, null]}\n'),
("CVM IASON\nCVM FRACTIO\nDIC(IASON_SCRIBE(IASON_LEGE('0.5')))",
Program([ModuleCall("IASON"), ModuleCall("FRACTIO")],
[ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE",
[BuiltIn("IASON_LEGE", [String("0.5")])])]))]),
ValStr("0.5"), "0.5\n"),
("CVM IASON\nCVM FRACTIO\nDIC(IASON_SCRIBE(IASON_LEGE('0.1')))",
Program([ModuleCall("IASON"), ModuleCall("FRACTIO")],
[ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE",
[BuiltIn("IASON_LEGE", [String("0.1")])])]))]),
ValStr("0.1"), "0.1\n"),
# ---- Serialize: insertion order preserved ----
(_src_scribe('TABVLA {"b" VT II, "a" VT I, "c" VT III}'),
_scribe(DataDict([
(String("b"), Numeral("II")),
(String("a"), Numeral("I")),
(String("c"), Numeral("III")),
])),
ValStr('{"b": 2, "a": 1, "c": 3}')),
# ---- Whitespace-tolerant parse ----
(_src_lege(" [ 1 , 2 ] "),
_lege(" [ 1 , 2 ] "),
ValList([ValInt(1), ValInt(2)])),
# ---- Unicode passes through serialize (ensure_ascii=False) ----
('CVM IASON\nDIC(IASON_SCRIBE("café"))',
Program([ModuleCall("IASON")], [ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE", [String("café")])]))]),
ValStr('"café"'), '"café"\n'),
]
class TestIason(unittest.TestCase):
@parameterized.expand(iason_tests)
def test_iason(self, source, nodes, value, output="", input_lines=[]):
run_test(self, source, nodes, value, output, input_lines)

225
tests/13_test_failures.py Normal file
View File

@@ -0,0 +1,225 @@
from tests._helpers import (
unittest, parameterized, Fraction, time,
run_test, run_compiler_error_test,
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _IASON_C, _cent_rng,
Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch,
)
# --- Errors ---
error_tests = [
("x", CentvrionError), # undefined variable
("INVOCA f ()", CentvrionError), # undefined function
("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA
("DESIGNA x III", SyntaxError), # parse error: missing VT
("DEFINI f () VT { REDI(I) }\nf()", SyntaxError), # function call without INVOCA (no args)
("DEFINI f (x) VT { REDI(x) }\nf(I)", SyntaxError), # function call without INVOCA (with args)
("DIC(M + M + M + M)", CentvrionError), # output > 3999 without MAGNVM
("IIII", CentvrionError), # invalid Roman numeral in source
("FORTVITVS_NVMERVS(I, X)", CentvrionError), # requires FORS module
('NVMERVS(I)', CentvrionError), # NVMERVS expects a string, not int
('NVMERVS("ABC")', CentvrionError), # invalid Roman numeral string
('NVMERVS("XIV", "IX")', CentvrionError), # too many args
("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", CentvrionError), # too many args
("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", CentvrionError), # too few args
("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", CentvrionError), # args to zero-param function
("SI NVLLVS TVNC { DESIGNA r VT I }", CentvrionError), # NVLLVS cannot be used as boolean
("NVLLVS AVT VERITAS", CentvrionError), # NVLLVS cannot be used as boolean in AVT
("FALSITAS AVT NVLLVS", CentvrionError), # no short-circuit: right side evaluated, NVLLVS not boolean
("VERITAS ET NVLLVS", CentvrionError), # no short-circuit: right side evaluated, NVLLVS not boolean
("NVLLVS ET VERITAS", CentvrionError), # NVLLVS cannot be used as boolean in ET
('I @ [II]', CentvrionError), # @ requires two arrays (int @ array)
('[I] @ "hello"', CentvrionError), # @ requires two arrays (array @ string)
('"a" @ "b"', CentvrionError), # @ requires two arrays (string @ string)
('"hello" + " world"', CentvrionError), # use & for string concatenation, not +
("[I, II][III]", CentvrionError), # index too high
("CVM SVBNVLLA\n[I, II][-I]", CentvrionError), # negative index
("[I, II][-I]", CentvrionError), # negative value
("I / NVLLVS", CentvrionError), # division by zero (NVLLVS coerces to 0)
("V RELIQVVM NVLLVS", CentvrionError), # modulo by zero (NVLLVS coerces to 0)
("NVLLVS RELIQVVM NVLLVS", CentvrionError), # modulo by zero (both NVLLVS)
("I / [I, II]", CentvrionError), # division with array operand
("I - \"hello\"", CentvrionError), # subtraction with string
("I * \"hello\"", CentvrionError), # multiplication with string
("\"hello\" MINVS \"world\"", CentvrionError), # comparison with strings
('"a" HAVD_PLVS "b"', CentvrionError), # HAVD_PLVS on strings
('[I] HAVD_MINVS [II]', CentvrionError), # HAVD_MINVS on arrays
("I[I]", CentvrionError), # indexing a non-array
('"SALVTE"[VII]', CentvrionError), # string index out of range
('"SALVTE"[NVLLVS]', CentvrionError), # string index with non-integer
('"SALVTE"[II VSQVE VII]', CentvrionError), # string slice out of range
('"SALVTE"[III VSQVE II]', CentvrionError), # string slice from > to
("DESIGNA x VT I\nDESIGNA x[I] VT II", CentvrionError), # index-assign to non-array
("SEMEN(I)", CentvrionError), # requires FORS module
('CVM FORS\nSEMEN("abc")', CentvrionError), # SEMEN requires integer seed
("FORTVITA_ELECTIO([])", CentvrionError), # FORS required for FORTVITA_ELECTIO
("CVM FORS\nFORTVITA_ELECTIO([])", CentvrionError), # FORTVITA_ELECTIO on empty array
("CVM FORS\nFORTVITVS_NVMERVS(X, I)", CentvrionError), # FORTVITVS_NVMERVS a > b
("PER i IN I FAC { DIC(i) }", CentvrionError), # PER over non-array
("DECIMATIO([I, II, III])", CentvrionError), # FORS required for DECIMATIO
("CVM FORS\nDECIMATIO(I)", CentvrionError), # DECIMATIO requires an array
("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array
("ORDINA(I)", CentvrionError), # ORDINA on non-array
('ORDINA([I, "a"])', CentvrionError), # ORDINA mixed types
("DESIGNA x VT I\nORDINA(x)", CentvrionError), # ORDINA on id (non-array)
("ORDINA([I, II], V)", CentvrionError), # ORDINA comparator not a function
("DEFINI bad (a) VT { REDI (VERITAS) }\nORDINA([I, II], bad)", CentvrionError), # ORDINA comparator wrong arity
("DEFINI bad (a, b) VT { REDI (V) }\nORDINA([I, II], bad)", CentvrionError), # ORDINA comparator returns non-bool
("ORDINA([I], V, V)", CentvrionError), # ORDINA too many args
("MVTA([I, II])", CentvrionError), # MVTA too few args
("MVTA([I, II], FVNCTIO (x) VT { REDI (x) }, V)", CentvrionError), # MVTA too many args
("MVTA(I, FVNCTIO (x) VT { REDI (x) })", CentvrionError), # MVTA on non-array
("MVTA([I, II], V)", CentvrionError), # MVTA function arg not a function
("DEFINI bad (a, b) VT { REDI (a) }\nMVTA([I, II], bad)", CentvrionError), # MVTA function wrong arity
("CRIBRA([I, II])", CentvrionError), # CRIBRA too few args
("CRIBRA([I, II], FVNCTIO (x) VT { REDI (VERITAS) }, V)", CentvrionError), # CRIBRA too many args
("CRIBRA(I, FVNCTIO (x) VT { REDI (VERITAS) })", CentvrionError), # CRIBRA on non-array
("CRIBRA([I, II], V)", CentvrionError), # CRIBRA predicate not a function
("DEFINI bad (a, b) VT { REDI (VERITAS) }\nCRIBRA([I, II], bad)", CentvrionError), # CRIBRA predicate wrong arity
("DEFINI bad (x) VT { REDI (V) }\nCRIBRA([I, II], bad)", CentvrionError), # CRIBRA predicate returns non-bool
("CONFLA([I, II], I)", CentvrionError), # CONFLA too few args
("CONFLA([I, II], I, FVNCTIO (a, b) VT { REDI (a + b) }, V)", CentvrionError), # CONFLA too many args
("CONFLA(I, I, FVNCTIO (a, b) VT { REDI (a + b) })", CentvrionError), # CONFLA on non-array
("CONFLA([I, II], I, V)", CentvrionError), # CONFLA function arg not a function
("DEFINI bad (a) VT { REDI (a) }\nCONFLA([I, II], I, bad)", CentvrionError), # CONFLA function wrong arity
("SENATVS(I)", CentvrionError), # SENATVS requires booleans
("SENATVS(VERITAS, I)", CentvrionError), # SENATVS mixed types
("SENATVS([I, II, III])", CentvrionError), # SENATVS array of non-bools
('LEGE("x.txt")', CentvrionError), # SCRIPTA required for LEGE
('SCRIBE("x.txt", "hi")', CentvrionError), # SCRIPTA required for SCRIBE
('ADIVNGE("x.txt", "hi")', CentvrionError), # SCRIPTA required for ADIVNGE
("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function
("SI I TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: int
("IIIS", CentvrionError), # fraction without FRACTIO module
("CVM FRACTIO\n[I, II, III][IIIS]", CentvrionError), # fractional index (IIIS = 7/2)
("CVM FRACTIO\n[I, II, III][I / II]", CentvrionError), # fractional index from division (1/2)
("DESIGNA z VT I - I\nSI z TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: zero int
("SI [I] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: non-empty list
("SI [] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: empty list
("DESIGNA x VT I\nDVM x FAC {\nDESIGNA x VT x + I\n}", CentvrionError), # non-bool DVM condition: int
("NON I", CentvrionError), # NON on integer
("DESIGNA z VT I - I\nNON z", CentvrionError), # NON on zero integer
('NON "hello"', CentvrionError), # NON on string
("DESIGNA a, b VT III", CentvrionError), # destructure non-array
("DESIGNA a, b VT [I]", CentvrionError), # destructure length mismatch: too many targets
("DESIGNA a, b VT [I, II, III]", CentvrionError), # destructure length mismatch: too few targets
("PER a, b IN [I, II, III] FAC { DIC(a) }", CentvrionError), # PER destructure: element is not an array
("PER a, b IN [[I], [II]] FAC { DIC(a) }", CentvrionError), # PER destructure: wrong number of elements
("[I, II, III][II VSQVE IV]", CentvrionError), # slice upper bound out of range
("[I, II, III][NVLLVS VSQVE II]", CentvrionError), # slice with non-integer bound
("I[I VSQVE II]", CentvrionError), # slice on non-array
("[I, II, III][III VSQVE I]", CentvrionError), # slice from > to
("CVM SVBNVLLA\n[I, II, III][-I VSQVE II]", CentvrionError), # slice with negative lower bound
("CVM SVBNVLLA\n[I, II, III][I VSQVE -I]", CentvrionError), # slice with negative upper bound
("CVM FRACTIO\n[I, II, III][IIIS VSQVE III]", CentvrionError), # slice with fractional lower bound
("CVM FRACTIO\n[I, II, III][I VSQVE IIIS]", CentvrionError), # slice with fractional upper bound
("CVM FRACTIO\n[I, II, III][I / II VSQVE III]", CentvrionError), # slice with division-fraction lower bound
("TEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA y VT I / NVLLVS\n}", CentvrionError), # uncaught error in catch block propagates
('QVAERE(I, "abc")', CentvrionError), # QVAERE requires strings, not int
('QVAERE("abc", I)', CentvrionError), # QVAERE requires strings, not int
('QVAERE("[", "abc")', CentvrionError), # QVAERE invalid regex
('SVBSTITVE(I, "b", "c")', CentvrionError), # SVBSTITVE requires strings, not int pattern
('SVBSTITVE("a", I, "c")', CentvrionError), # SVBSTITVE requires strings, not int replacement
('SVBSTITVE("a", "b", I)', CentvrionError), # SVBSTITVE requires strings, not int text
('SVBSTITVE("[", "b", "c")', CentvrionError), # SVBSTITVE invalid regex
("SVBSTITVE('(a)', '\\1', 'a')", CentvrionError), # Arabic backref in replacement
("QVAERE('(.)\\1', 'aa')", CentvrionError), # Arabic backref in pattern
("QVAERE('a{3}', 'aaa')", CentvrionError), # Arabic quantifier in pattern
('SCINDE(I, ",")', CentvrionError), # SCINDE requires strings, not int
('SCINDE("a", I)', CentvrionError), # SCINDE requires strings, not int delimiter
('MAIVSCVLA(I)', CentvrionError), # MAIVSCVLA requires a string, not int
('MAIVSCVLA()', CentvrionError), # MAIVSCVLA requires exactly 1 arg
('MAIVSCVLA("a", "b")', CentvrionError), # MAIVSCVLA too many args
('MINVSCVLA(I)', CentvrionError), # MINVSCVLA requires a string, not int
('MINVSCVLA()', CentvrionError), # MINVSCVLA requires exactly 1 arg
('MINVSCVLA("a", "b")', CentvrionError), # MINVSCVLA too many args
('PETE("http://example.com")', CentvrionError), # RETE required for PETE
('CVM RETE\nPETE(I)', CentvrionError), # PETE requires a string URL
('PETITVR("/", FVNCTIO (r) VT {\nREDI("hi")\n})', CentvrionError), # RETE required for PETITVR
('CVM RETE\nPETITVR(I, FVNCTIO (r) VT {\nREDI("hi")\n})', CentvrionError), # PETITVR path must be string
('CVM RETE\nPETITVR("/", "not a func")', CentvrionError), # PETITVR handler must be function
('CVM RETE\nAVSCVLTA(LXXX)', CentvrionError), # AVSCVLTA: no routes registered
('AVSCVLTA(LXXX)', CentvrionError), # RETE required for AVSCVLTA
('CVM RETE\nPETITVR("/", FVNCTIO (r) VT {\nREDI("hi")\n})\nAVSCVLTA("text")', CentvrionError), # AVSCVLTA port must be integer
("DONICVM i VT I VSQVE X GRADV I - I FAC { DIC(i) }", CentvrionError), # GRADV zero step
('DONICVM i VT I VSQVE X GRADV "foo" FAC { DIC(i) }', CentvrionError), # GRADV non-integer step
("NECTE([I, II], [III])", CentvrionError), # NECTE length mismatch
('NECTE(I, [II])', CentvrionError), # NECTE first arg not a list
('NECTE([I], II)', CentvrionError), # NECTE second arg not a list
("IVNGE([I, II], [III])", CentvrionError), # IVNGE length mismatch
('IVNGE(I, [II])', CentvrionError), # IVNGE first arg not a list
('IVNGE(["a"], II)', CentvrionError), # IVNGE second arg not a list
("IVNGE([VERITAS], [I])", CentvrionError), # IVNGE invalid key type (bool)
("IVNGE([[I]], [II])", CentvrionError), # IVNGE invalid key type (list)
("IASON_LEGE('null')", CentvrionError), # IASON module required for IASON_LEGE
("IASON_SCRIBE(NVLLVS)", CentvrionError), # IASON module required for IASON_SCRIBE
("CVM IASON\nIASON_LEGE(I)", CentvrionError), # IASON_LEGE non-string arg
("CVM IASON\nIASON_LEGE()", CentvrionError), # IASON_LEGE no args
("CVM IASON\nIASON_LEGE('null', 'null')", CentvrionError), # IASON_LEGE too many args
("CVM IASON\nIASON_LEGE('not json')", CentvrionError), # invalid JSON
("CVM IASON\nIASON_LEGE('[1,]')", CentvrionError), # trailing comma in array
("CVM IASON\nIASON_LEGE('{\"a\":}')", CentvrionError), # missing value in object
("CVM IASON\nIASON_LEGE('{\"a\" 1}')", CentvrionError), # missing colon in object
("CVM IASON\nIASON_LEGE('[1, 2')", CentvrionError), # unterminated array
("CVM IASON\nIASON_LEGE('{')", CentvrionError), # unterminated object
("CVM IASON\nIASON_LEGE('\"abc')", CentvrionError), # unterminated string
("CVM IASON\nIASON_LEGE('[1] junk')", CentvrionError), # trailing data
("CVM IASON\nIASON_LEGE('[\"a\\\\x\"]')", CentvrionError), # invalid escape
("CVM IASON\nIASON_SCRIBE()", CentvrionError), # IASON_SCRIBE no args
("CVM IASON\nIASON_SCRIBE(I, II)", CentvrionError), # IASON_SCRIBE too many args
('CVM IASON\nIASON_SCRIBE(IVNGE([I], ["v"]))', CentvrionError), # IASON_SCRIBE int dict keys
("CVM IASON\nIASON_SCRIBE(FVNCTIO (x) VT { REDI(x) })", CentvrionError), # IASON_SCRIBE function
]
class TestErrors(unittest.TestCase):
@parameterized.expand(error_tests)
def test_errors(self, source, error_type):
with self.assertRaises(error_type):
run_test(self, source, None, None)
compiler_error_tests = [(s, e) for s, e in error_tests if e == CentvrionError]
class TestCompilerErrors(unittest.TestCase):
@parameterized.expand(compiler_error_tests)
def test_compiler_errors(self, source, error_type):
run_compiler_error_test(self, source)
class TestErrorLineNumbers(unittest.TestCase):
def test_interpreter_error_includes_line(self):
source = "DESIGNA x VT III\nDIC(y)\n"
tokens = Lexer().get_lexer().lex(source)
program = Parser().parse(tokens)
with self.assertRaisesRegex(CentvrionError, r"at line 2"):
program.eval()
def test_compiled_error_includes_line(self):
source = "DESIGNA x VT III\nDIC(y)\n"
tokens = Lexer().get_lexer().lex(source)
program = Parser().parse(tokens)
c_source = compile_program(program)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, _IASON_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, capture_output=True,
)
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
self.assertNotEqual(proc.returncode, 0)
self.assertIn("at line 2", proc.stderr)
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)

0
tests/__init__.py Normal file
View File

137
tests/_helpers.py Normal file
View File

@@ -0,0 +1,137 @@
import os
import subprocess
import tempfile
import time
import unittest
from io import StringIO
from unittest.mock import patch
from parameterized import parameterized
from fractions import Fraction
from centvrion.ast_nodes import (
ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement,
Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca,
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
_cent_rng,
)
from centvrion.compiler.emitter import compile_program
from centvrion.errors import CentvrionError
from centvrion.lexer import Lexer
from centvrion.parser import Parser
from centvrion.values import ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac
_RUNTIME_DIR = os.path.join(
os.path.dirname(__file__), "..",
"centvrion", "compiler", "runtime"
)
_RUNTIME_C = os.path.join(_RUNTIME_DIR, "cent_runtime.c")
_IASON_C = os.path.join(_RUNTIME_DIR, "cent_iason.c")
def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
_cent_rng.seed(1)
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
program = Parser().parse(tokens)
##########################
####### Parser Test ######
##########################
if target_nodes is not None:
self.assertEqual(
program,
target_nodes,
f"Parser test:\n{program}\n{target_nodes}"
)
##########################
#### Interpreter Test ####
##########################
captured = StringIO()
try:
if input_lines:
inputs = iter(input_lines)
with patch("builtins.input", lambda: next(inputs)), patch("sys.stdout", captured):
result = program.eval()
else:
with patch("sys.stdout", captured):
result = program.eval()
except Exception as e:
raise e
self.assertEqual(result, target_value, "Return value test")
self.assertEqual(captured.getvalue(), target_output, "Output test")
##########################
###### Printer Test ######
##########################
try:
new_text = program.print()
new_tokens = Lexer().get_lexer().lex(new_text + "\n")
new_nodes = Parser().parse(new_tokens)
except Exception as e:
raise Exception(f"###Printer test###\n{new_text}") from e
self.assertEqual(
program,
new_nodes,
f"Printer test\n{source}\n{new_text}"
)
##########################
###### Compiler Test #####
##########################
c_source = compile_program(program)
# Force deterministic RNG seed=1 for test reproducibility
c_source = c_source.replace("cent_init();", "cent_init(); cent_semen((CentValue){.type=CENT_INT, .ival=1});", 1)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, _IASON_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, capture_output=True,
)
stdin_data = "".join(f"{l}\n" for l in input_lines)
proc = subprocess.run(
[tmp_bin_path],
input=stdin_data, capture_output=True, text=True,
)
self.assertEqual(proc.returncode, 0, f"Compiler binary exited non-zero:\n{proc.stderr}")
self.assertEqual(proc.stdout, target_output, "Compiler output test")
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
assert target_nodes is not None, "All tests must have target nodes"
def run_compiler_error_test(self, source):
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
program = Parser().parse(tokens)
try:
c_source = compile_program(program)
except CentvrionError:
return # compile-time detection is valid
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, _IASON_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, capture_output=True,
)
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
self.assertNotEqual(proc.returncode, 0, "Expected non-zero exit for error program")
self.assertTrue(proc.stderr.strip(), "Expected error message on stderr")
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)

View File

@@ -20,6 +20,10 @@
"language": "cent",
"scopeName": "source.cent",
"path": "./syntaxes/cent.tmLanguage.json"
}],
"snippets": [{
"language": "cent",
"path": "./snippets/cent.json"
}]
}
}

View File

@@ -0,0 +1,98 @@
{
"SI": {
"prefix": "SI",
"body": ["SI ${1:condicio} TVNC {", " $0", "}"],
"description": "if/then conditional"
},
"DVM": {
"prefix": "DVM",
"body": ["DVM ${1:condicio} FAC {", " $0", "}"],
"description": "while loop"
},
"PER": {
"prefix": "PER",
"body": ["PER ${1:elementum} IN ${2:tabula} FAC {", " $0", "}"],
"description": "for-each loop"
},
"DEFINI": {
"prefix": "DEFINI",
"body": ["DEFINI ${1:nomen} ${2:parametri} VT {", " $0", "}"],
"description": "define named function"
},
"DONICVM": {
"prefix": "DONICVM",
"body": ["DONICVM ${1:i} VT ${2:I} VSQVE ${3:X} FAC {", " $0", "}"],
"description": "numeric range loop"
},
"AETERNVM": { "prefix": "AETERNVM", "body": "AETERNVM", "description": "infinite loop" },
"ALIVD": { "prefix": "ALIVD", "body": "ALIVD", "description": "else / else-if branch" },
"AVGE": { "prefix": "AVGE", "body": "AVGE", "description": "compound addition assignment (+=)" },
"AVT": { "prefix": "AVT", "body": "AVT", "description": "logical or" },
"CAPE": { "prefix": "CAPE", "body": "CAPE", "description": "catch block header" },
"CONTINVA": { "prefix": "CONTINVA", "body": "CONTINVA", "description": "skip to next loop iteration" },
"CVM": { "prefix": "CVM", "body": "CVM", "description": "include module" },
"DESIGNA": { "prefix": "DESIGNA", "body": "DESIGNA", "description": "declare and assign a variable" },
"DISPAR": { "prefix": "DISPAR", "body": "DISPAR", "description": "not-equal comparison (!=)" },
"DIVIDE": { "prefix": "DIVIDE", "body": "DIVIDE", "description": "compound division assignment (/=)" },
"ERVMPE": { "prefix": "ERVMPE", "body": "ERVMPE", "description": "break out of loop" },
"EST": { "prefix": "EST", "body": "EST", "description": "equality comparison (==)" },
"ET": { "prefix": "ET", "body": "ET", "description": "logical and" },
"FAC": { "prefix": "FAC", "body": "FAC", "description": "block opener for loops and functions" },
"FALSITAS": { "prefix": "FALSITAS", "body": "FALSITAS", "description": "boolean false" },
"FVNCTIO": { "prefix": "FVNCTIO", "body": "FVNCTIO", "description": "anonymous function expression" },
"GRADV": { "prefix": "GRADV", "body": "GRADV", "description": "DONICVM range step (by a step of)" },
"HAVD_MINVS": { "prefix": "HAVD_MINVS", "body": "HAVD_MINVS", "description": "greater-or-equal (>=)" },
"HAVD_PLVS": { "prefix": "HAVD_PLVS", "body": "HAVD_PLVS", "description": "less-or-equal (<=)" },
"IN": { "prefix": "IN", "body": "IN", "description": "PER-loop iterator separator" },
"INVOCA": { "prefix": "INVOCA", "body": "INVOCA", "description": "invoke / call a function" },
"MINVE": { "prefix": "MINVE", "body": "MINVE", "description": "compound subtraction assignment (-=)" },
"MINVS": { "prefix": "MINVS", "body": "MINVS", "description": "less-than comparison (<)" },
"MVLTIPLICA": { "prefix": "MVLTIPLICA", "body": "MVLTIPLICA", "description": "compound multiplication assignment (*=)" },
"NON": { "prefix": "NON", "body": "NON", "description": "logical not" },
"NVLLVS": { "prefix": "NVLLVS", "body": "NVLLVS", "description": "null value" },
"PLVS": { "prefix": "PLVS", "body": "PLVS", "description": "greater-than comparison (>)" },
"REDI": { "prefix": "REDI", "body": "REDI", "description": "return from function" },
"RELIQVVM": { "prefix": "RELIQVVM", "body": "RELIQVVM", "description": "modulo / remainder" },
"TABVLA": { "prefix": "TABVLA", "body": "TABVLA", "description": "dict literal opener" },
"TEMPTA": { "prefix": "TEMPTA", "body": "TEMPTA", "description": "try block header" },
"TVNC": { "prefix": "TVNC", "body": "TVNC", "description": "then (SI branch)" },
"VERITAS": { "prefix": "VERITAS", "body": "VERITAS", "description": "boolean true" },
"VSQVE": { "prefix": "VSQVE", "body": "VSQVE", "description": "upper bound of DONICVM range" },
"VT": { "prefix": "VT", "body": "VT", "description": "assignment / binding connective" },
"ADIVNGE": { "prefix": "ADIVNGE", "body": "ADIVNGE", "description": "append string to file (SCRIPTA module)" },
"AVDI": { "prefix": "AVDI", "body": "AVDI", "description": "read a line from stdin" },
"AVDI_NVMERVS": { "prefix": "AVDI_NVMERVS", "body": "AVDI_NVMERVS", "description": "read a Roman numeral from stdin" },
"AVSCVLTA": { "prefix": "AVSCVLTA", "body": "AVSCVLTA", "description": "start HTTP server on port (RETE module)" },
"CLAVES": { "prefix": "CLAVES", "body": "CLAVES", "description": "return the keys of a dict" },
"DECIMATIO": { "prefix": "DECIMATIO", "body": "DECIMATIO", "description": "remove a random tenth of an array (FORS module)" },
"DIC": { "prefix": "DIC", "body": "DIC", "description": "print value(s) to stdout" },
"DORMI": { "prefix": "DORMI", "body": "DORMI", "description": "sleep for N seconds" },
"EVERRE": { "prefix": "EVERRE", "body": "EVERRE", "description": "clear the terminal screen" },
"FORTVITA_ELECTIO": { "prefix": "FORTVITA_ELECTIO", "body": "FORTVITA_ELECTIO", "description": "pick a random element from an array (FORS module)" },
"FORTVITVS_NVMERVS": { "prefix": "FORTVITVS_NVMERVS", "body": "FORTVITVS_NVMERVS", "description": "random integer in a range (FORS module)" },
"LEGE": { "prefix": "LEGE", "body": "LEGE", "description": "read file contents (SCRIPTA module)" },
"LITTERA": { "prefix": "LITTERA", "body": "LITTERA", "description": "coerce any value to its display string" },
"LONGITVDO": { "prefix": "LONGITVDO", "body": "LONGITVDO", "description": "length of array, string, or dict" },
"MAIVSCVLA": { "prefix": "MAIVSCVLA", "body": "MAIVSCVLA", "description": "uppercase a string (ASCII a-z → A-Z)" },
"MINVSCVLA": { "prefix": "MINVSCVLA", "body": "MINVSCVLA", "description": "lowercase a string (ASCII A-Z → a-z)" },
"NVMERVS": { "prefix": "NVMERVS", "body": "NVMERVS", "description": "parse a Roman numeral string to an integer" },
"ORDINA": { "prefix": "ORDINA", "body": "ORDINA", "description": "sort an array in ascending order" },
"PETE": { "prefix": "PETE", "body": "PETE", "description": "HTTP GET request (RETE module)" },
"PETITVR": { "prefix": "PETITVR", "body": "PETITVR", "description": "register HTTP GET handler (RETE module)" },
"QVAERE": { "prefix": "QVAERE", "body": "QVAERE", "description": "regex findall" },
"SCINDE": { "prefix": "SCINDE", "body": "SCINDE", "description": "split a string by delimiter" },
"SCRIBE": { "prefix": "SCRIBE", "body": "SCRIBE", "description": "write string to file (SCRIPTA module)" },
"SEMEN": { "prefix": "SEMEN", "body": "SEMEN", "description": "seed the random number generator (FORS module)" },
"SENATVS": { "prefix": "SENATVS", "body": "SENATVS", "description": "true if a strict majority of booleans are true" },
"SVBSTITVE": { "prefix": "SVBSTITVE", "body": "SVBSTITVE", "description": "regex substitute" },
"TYPVS": { "prefix": "TYPVS", "body": "TYPVS", "description": "return the type of a value as a string" },
"FORS": { "prefix": "FORS", "body": "FORS", "description": "randomness module" },
"FRACTIO": { "prefix": "FRACTIO", "body": "FRACTIO", "description": "base-12 fractions module" },
"MAGNVM": { "prefix": "MAGNVM", "body": "MAGNVM", "description": "large-integer module (thousands suffix _)" },
"RETE": { "prefix": "RETE", "body": "RETE", "description": "networking module (HTTP)" },
"SCRIPTA": { "prefix": "SCRIPTA", "body": "SCRIPTA", "description": "file I/O module" },
"SVBNVLLA": { "prefix": "SVBNVLLA", "body": "SVBNVLLA", "description": "negative-number module (-II syntax)" }
}

View File

@@ -8,9 +8,6 @@
{
"include": "#strings"
},
{
"include": "#fractions"
},
{
"include": "#keywords"
},
@@ -20,6 +17,9 @@
{
"include": "#modules"
},
{
"include": "#fractions"
},
{
"include": "#constants"
},
@@ -45,11 +45,11 @@
"patterns": [
{
"name": "keyword.control.cent",
"match": "\\b(AETERNVM|ALIVD|AVT|CONTINVA|CVM|DEFINI|DESIGNA|DONICVM|DVM|ERVMPE|ET|FAC|FVNCTIO|IN|INVOCA|NON|PER|REDI|SI|TVNC|VSQVE|VT)\\b"
"match": "\\b(AETERNVM|ALIVD|AVGE|AVT|CAPE|CONTINVA|CVM|DEFINI|DESIGNA|DIVIDE|DONICVM|DVM|ERVMPE|ET|FAC|FVNCTIO|GRADV|IN|INVOCA|MINVE|MVLTIPLICA|NON|PER|REDI|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT)\\b"
},
{
"name": "keyword.operator.comparison.cent",
"match": "\\b(DISPAR|EST|MINVS|PLVS)\\b"
"match": "\\b(HAVD_PLVS|HAVD_MINVS|DISPAR|EST|MINVS|PLVS)\\b"
},
{
"name": "keyword.operator.arithmetic.cent",
@@ -57,7 +57,7 @@
},
{
"name": "keyword.operator.arithmetic.cent",
"match": "(\\*|\\+|-|/|&)"
"match": "(\\*|\\+|-|/|&|@)"
}
]
},
@@ -65,7 +65,7 @@
"patterns": [
{
"name": "support.function.builtin.cent",
"match": "\\b(AVDI_NVMERVS|AVDI|DECIMATIO|DIC|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|LONGITVDO|SEMEN)\\b"
"match": "\\b(ADDE|ADIVNGE|AVDI_NVMERVS|AVDI|AVSCVLTA|CLAVES|CONFLA|CRIBRA|DECIMATIO|DIC|DORMI|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|IASON_LEGE|IASON_SCRIBE|INSERE|IVNGE|LEGE|LITTERA|LONGITVDO|MAIVSCVLA|MINVSCVLA|MVTA|NECTE|NVMERVS|ORDINA|PETE|PETITVR|QVAERE|SCINDE|SCRIBE|SEMEN|SENATVS|SVBSTITVE|TOLLE|TYPVS)\\b"
}
]
},
@@ -73,7 +73,7 @@
"patterns": [
{
"name": "support.class.module.cent",
"match": "\\b(FORS|FRACTIO|MAGNVM|SVBNVLLA)\\b"
"match": "\\b(FORS|FRACTIO|IASON|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\\b"
}
]
},