├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── arrays.md ├── assets │ ├── icon-black.png │ ├── icon-white.png │ ├── icon.png │ ├── script.js │ └── style.css ├── builtins.md ├── classes.md ├── comments.md ├── controlflow.md ├── enums.md ├── examples.md ├── fileio.md ├── functions.md ├── index.md ├── interop.md ├── modules.md ├── null.md ├── numbers.md ├── operators.md ├── slices.md ├── stdcollections.md ├── stddatetime.md ├── stdio.md ├── stditer.md ├── stdmath.md ├── stdoperator.md ├── stdrandom.md ├── stdstring.md ├── stdtypes.md ├── strings.md ├── tables.md ├── tools.md ├── variables.md └── zip.md ├── justfile ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml └── samarium ├── __init__.py ├── __main__.py ├── builtins.py ├── classes ├── __init__.py ├── base.py └── fileio.py ├── core.py ├── exceptions.py ├── imports.py ├── modules ├── collections.sm ├── datetime.sm ├── io.sm ├── iter.sm ├── math.sm ├── operator.sm ├── pystd.py ├── random.sm ├── string.sm └── types.sm ├── python.py ├── repl.py ├── runtime.py ├── template.txt ├── tokenizer.py ├── tokens.py ├── transpiler.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .mypy_cache 3 | __pycache__ 4 | test 5 | dist/ 6 | site/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.2] - 2024-06-19 9 | 10 | ### Changed 11 | - Session names now only allow a limited set of characters (`[a-zA-Z0-9_\-]`) 12 | 13 | ### Fixed 14 | - `:session load` will no longer crash on a non-existent session 15 | 16 | ## [0.6.1] - 2024-06-18 17 | 18 | ### Changed 19 | - Rewrote REPL config handling to not use the `configzen` library 20 | 21 | ## [0.6.0] - 2024-06-14 22 | 23 | ### Added 24 | - `=>!` now accepts strings 25 | - Added an exception note for `string.split` 26 | - Allowed adding notes for `!!!` 27 | - Command system for the REPL 28 | - Data classes 29 | - Function composition 30 | - Made the semicolon optional for `!`, `!!!`, `*`, `**` 31 | - `math.max` and `math.min` now work with varargs 32 | - `String+` 33 | - `-String` 34 | - `~Table` 35 | - The REPL now automatically displays most expressions 36 | 37 | ### Changed 38 | - `String ++ -Number` now throws a type error (ambiguous operation) 39 | 40 | ### Fixed 41 | - Fixed function conversion when `@export`ing 42 | - Fixed internal name of `math.round` 43 | - Fixed `math.sqrt` using an incorrect variable name 44 | - Fixed multiline string handling in the REPL 45 | - Fixed REPL quitting on syntax error 46 | - `math.round` now correctly provides a default value for `ndigits` 47 | - The transpiler now correctly disallows literals around identifiers 48 | - `types.Boolean` should now correctly interract with external types 49 | - Various minor fixes to the transpiler 50 | 51 | ## [0.5.3] - 2023-04-21 52 | 53 | ### Fixed 54 | - Fixed entrypoint argc check template 55 | 56 | ## [0.5.2] - 2023-04-21 57 | 58 | ### Fixed 59 | - Corrected `types.Number` type alias name 60 | 61 | ## [0.5.1] - 2023-04-08 62 | 63 | ### Added 64 | - `~` is now equal to `-1` when not followed by a value (e.g. `~;` or `~<`, but 65 | not `~x` or `~/\`) 66 | 67 | ### Fixed 68 | - Implicit null is no longer placed before `~` (meaning that `>~<` is no longer 69 | invalid syntax) 70 | 71 | ## [0.5.0] - 2023-04-02 72 | 73 | ### Added 74 | - `collections.Queue` now supports membership checking 75 | - `collections.Set` now uses the built-in set-based Array operations (working 76 | with small sets can be up to 3,600x faster) 77 | - Files are now iterable 78 | - `iter.filter` and `iter.map` now adapt to functions based on their number of 79 | parameters 80 | - `math.is_int` 81 | - `math.ceil` 82 | - `math.floor` 83 | - `math.round` 84 | - `math.to_bin` (to replace `Integer$`) 85 | - Number type (in place of the Integer type): 86 | - supports floats 87 | - `x$` now floors the number 88 | - `x -- y` now does true division rather than floor division 89 | - Number literals, using `` ` `` as the decimal point (meaning that it's no 90 | longer ignored by the tokenizer) 91 | - Nulls can now be cast to Numbers 92 | - Parallel assignment 93 | - `random.sample` now supports slices 94 | - Set-based Array operations: 95 | - `-x` returns a copy of `x` with duplicates removed 96 | - `x -- y` ("difference") tries removing elements of `y` from `x`, even if 97 | they're not present 98 | - `x --- y` removes all duplicates of `y` in `x` 99 | - `x | y` ("union") creates an array with elements that appear in either `x` 100 | or `y` (duplicates possible) 101 | - `x & y` ("intersection") creates an array with elements that appear in both 102 | `x` and `y` (duplicates possible) 103 | - `x ^ y` ("symmetric difference") creates an array with elements that appear 104 | in either `x` or `y`, but not both (duplicates possible) 105 | - Slices are now hashable 106 | - `String -- String` (old `String - String` behavior) 107 | - `String++` (equivalent to `String ++ /\`) 108 | - `string.split` now accepts multiple separators 109 | - String→Number now supports scientific notation 110 | - `string.split_lines` 111 | - Strings can have their characters shifted by adding or subtracting Integers 112 | - `types.Frozen` 113 | 114 | ### Changed 115 | - Flipped argument order for `iter.reduce` 116 | - Improved Array/Slice/String index typechecking 117 | - Improved CLI error messages 118 | - Improved error messages 119 | - Improved `File.__str__` 120 | - Improved implicit null detection 121 | - Improved slice transpilation 122 | - `random.shuffle` now does type checking againast slices 123 | - `String - String` now removes only the first occurence 124 | - The main function is no longer required 125 | - Two or more consecutive logical NOTs are now considered a syntax error 126 | 127 | ### Fixed 128 | - Comments are now correctly tokenized 129 | - Fixed `...` special method not being detected 130 | - Fixed Slice→Array construction 131 | - Fixed some yield statements crashing the transpiler 132 | - Functions are now correctly displayed in Arrays/Tables 133 | - Logical NOT (`~~`) no longer lets Python's `bool` slip in 134 | - Multiline strings are now correctly tokenized 135 | - `Number ++ String` is no longer detected as an invalid operation 136 | - Slices are now correctly detected as objects directly after scope exit 137 | - `String.__repr__` now correctly handles escape sequences 138 | - `Type(a) != B` no longer yields incorrect results 139 | - `UserAttrs` can no longer show up when using `x!?` 140 | - Varargs functions now work correctly with recursive functions and decorators 141 | 142 | ### Removed 143 | - `collections.StaticArray` 144 | - Integer type 145 | 146 |
147 | 148 | The [Examples](https://samarium-lang.github.io/Samarium/examples/) page was also 149 | updated with new examples. 150 | 151 | Thanks to [@DancingGrumpyCat](https://github.com/DancingGrumpyCat) for improving the documentation! 152 | 153 | ## [0.4.0] - 2022-12-01 154 | 155 | ### Added 156 | - `Array%` 157 | - `Enum%` 158 | - `io` module: 159 | - `io.Bytes` 160 | - `io.inputcast` 161 | - `io.read_until` 162 | - `iter.cycle` by [@Lunarmagpie](https://github.com/Lunarmagpie) 163 | - New import system, including: 164 | - import aliases 165 | - inline imports 166 | - New special method syntax 167 | - Partial Python Interoperability by [@Endercheif](https://github.com/Endercheif) 168 | - `start` parameter for `math.sum`, thus allowing to sum non-Integers 169 | - Static methods (`~'*` keyword) 170 | - `string.ordinal` 171 | - `string.split()` now supports separators of length greater than 1, and also handles empty separators 172 | - Strings of length greater than 1 can now be cast into Arrays of Integers 173 | - Subtracting strings 174 | - Support for `^L`, `^[[A`, `^[[B`, `^[[C`, `^[[D` in the REPL 175 | - `to_bit` methods for: 176 | - `Enum` 177 | - `File` 178 | - `Iterator` 179 | - `Module` 180 | - `Type`s and functions are now hashable 181 | - Zip `><` operator 182 | - `Zip` type 183 | 184 | ### Changed 185 | - Flipped argument order for: 186 | - `iter.map` 187 | - `iter.filter` 188 | - `iter.filter_false` 189 | - Greatly improved error messages 190 | - Improved `collections.Set` methods: 191 | - Replaced `union` with `|` 192 | - Replaced `intersection` with `&` 193 | - Replaced `difference` with `-` 194 | - Removed `is_subset` in place of `::`, `:::`, `>`, `<`, `>:`, `<:` operator support 195 | - Improved function to string conversion 196 | - Improved implicit null detection 197 | - Improved readline error handling 198 | - Improved slice object detection 199 | - Improved string integer parsing 200 | - Improved variable type verification 201 | - Rewrote the objects (should be 10–50% faster than Samarium 0.3.1 🚀) 202 | - Replaced the native tokenizer with a [crossandra](https://github.com/trag1c/crossandra) tokenizer (~3x faster tokenization 🚀) 203 | - Updated `to_string` methods of: 204 | - `collections.ArithmeticArray` 205 | - `collections.Deque` 206 | - `collections.Queue` 207 | - `collections.Set` 208 | 209 | ### Fixed 210 | - `collections.Set.#new_set_size` now takes unsized sets into consideration 211 | - Constructing `Slice`s from `Type` now correctly works 212 | - Error message shown when trying to run an unknown file is now written to stderr, not stdout 213 | - Syntax errors now cannot be caught 214 | 215 | ### Removed 216 | - `collections.Set.values()` (use `collections.Set.items`) 217 | - English special method names 218 | - `iter.zip` (use the `><` operator) 219 | - `iter.enumerate` (use the `><` operator with `<<>>`) 220 | - Some dead code :) 221 | - `string.format` (use `String`'s `---` operator) 222 | 223 | --- 224 | 225 | Also big thanks to [@qexat](https://github.com/qexat) & [@Celeo](https://github.com/Celeo) for code improvements! ❤️ 226 | 227 | ## [0.3.1] - 2022-09-23 228 | 229 | ### Fixed 230 | - Added missing cases for implicit null 231 | 232 | ### Removed 233 | - Unused imports & variables in the standard library 234 | - Unused slots in built-in types 235 | 236 | 237 | ## [0.3.0] - 2022-09-21 238 | 239 | ### Added 240 | - `**` (the yield statement) 241 | - Array support for `string.format` 242 | - Better error messages 243 | - `datetime` module by [@Endercheif](https://github.com/Endercheif) 244 | - Enums 245 | - `iter.chunks` 246 | - `iter.flatten` 247 | - `iter.sorted` 248 | - Iterators 249 | - Memory address lookup 250 | - Private variables 251 | - snake_case support 252 | - Substring support for `iter.find_all` 253 | 254 | ### Changed 255 | - `#` for assert has been replaced by `!!` 256 | - `@@` now returns the Unix timestamp 257 | - `@@@` now returns the date and time array 258 | - Classes can now be used as entry points 259 | - Improved hashing speed 260 | - Improved `iter` module speed by making it use Iterators over Arrays 261 | - Moved from [termcolor](https://pypi.org/project/termcolor/) to [Dahlia](https://github.com/trag1c/Dahlia) 262 | - Rewrote the transpiler 263 | - Slice objects are now iterable and can be used as ranges (about 3–7x faster than `iter.range` 🚀) 264 | - Slices now use `..` instead of `**` for the step delimiter (`<>` would be `<>`) 265 | - Class special method names were changed to snake_case 266 | - Standard Library function names were changed to snake_case 267 | - Standard Library constant names were changed to SCREAMING_SNAKE_CASE 268 | - `to_string` method doesn't need to be defined for the object to be printable (defaults to ``) 269 | 270 | ### Fixed 271 | - Nested slicing 272 | 273 | ### Removed 274 | - `_` as a null token 275 | - `iter.range` (use slices) 276 | - `iter.sort` (use `iter.sorted`) 277 | 278 | 279 | ## [0.2.3] - 2022-06-20 280 | 281 | ### Fixed 282 | - Fixed converting slices to strings 283 | 284 | 285 | ## [0.2.2] - 2022-06-18 286 | 287 | ### Fixed 288 | - Added a missing `argc` attribute for the template 289 | 290 | 291 | ## [0.2.1] - 2022-06-18 292 | 293 | ### Fixed 294 | - Class methods now correctly pass the instance 295 | 296 | 297 | ## [0.2.0] - 2022-06-13 298 | 299 | ### Added 300 | - `-h` / `--help` option 301 | - Argument unpacking 302 | - Decorators 303 | - Enriched modules: 304 | - `collections.ArithmeticArray` 305 | - `iter`: 306 | - `all` 307 | - `any` 308 | - `findAll` 309 | - `pairwise` 310 | - `zip` 311 | - `zipLongest` 312 | - `math.isPrime` 313 | - `random.randint` 314 | - `string.format` 315 | - `types`: 316 | - `Array` 317 | - `Integer` 318 | - `Null` 319 | - `Slice` 320 | - `String` 321 | - `Table` 322 | - `UUID4` 323 | - File descriptor support for standard stream access 324 | - Greatly improved object constructors for: 325 | - `Array`: 326 | - Default value: `[]` 327 | - `Array(a)` will now return a copy of the array `a` 328 | - Arrays can now be constructed from strings and tables: 329 | - `Array("ball") :: ["b", "a", "l", "l"]` 330 | - `Array({{// -> /\\/, "X" -> "D"}}) :: [[//, /\\/], ["X", "D"]]` 331 | - `Integer`: 332 | - Default value: `\` 333 | - Added support for binary, octal, hexadecimal representations: 334 | - `Integer("b:1000") :: Integer("8")` 335 | - `Integer("o:1000") :: Integer("512")` 336 | - `Integer("x:1000") :: Integer("4096")` 337 | - `String`: 338 | - Default value: `""` 339 | - `String(s)` will now return a copy of the string `s` 340 | - `Table`: 341 | - Default value: `{{}}` 342 | - `Table(t)` will now return a copy of the table `t` 343 | - Tables can be constructed from arrays containing 2-element iterables: 344 | - `Table([[//, /\\/], "XD"]) :: {{// -> /\\/, "X" -> "D"}}` 345 | - Introduced a new `Function` class for functions, replacing the old decorator 346 | - New function syntax: 347 | - `arg?` now makes the argument optional 348 | - optional arguments require setting the default value using the `arg <> default` statement inside the function body 349 | - `args...` will now accept a variable number of arguments and pass them to the function as an array 350 | - New `operator` module - containing standard operators as functions: 351 | - `add` (`x + y`) 352 | - `and` (`x & y`) 353 | - `cast` (`x%`) 354 | - `divide` (`x -- y`) 355 | - `equals` (`x :: y`) 356 | - `greaterThanOrEqual` (`x >: y`) 357 | - `greaterThan` (`x > y`) 358 | - `has` (`y ->? x`) 359 | - `hash` (`x##`) 360 | - `lessThanOrEqual` (`x <: y`) 361 | - `lessThan` (`x < y`) 362 | - `mod` (`x --- y`) 363 | - `multiplty` (`x ++ y`) 364 | - `not` (`~x`) 365 | - `notEquals` (`x ::: y`) 366 | - `or` (`x | y`) 367 | - `power` (`x +++ y`) 368 | - `random` (`x??`) 369 | - `special` (`x$`) 370 | - `subtract` (`x - y`) 371 | - `toBit` 372 | - `toString` 373 | - `xor` (`x ^ y`) 374 | - Project description in `pyproject.toml` 375 | - Samarium REPL 376 | - Separate error for IO operations: `IOError` 377 | - Some objects now have a `.random` method, supported by the `object??` syntax: 378 | - `array??` will return a random element 379 | - `integer??` will return: 380 | - a number in range `[0, n)` for positive values 381 | - a number in range `[n, 0)` for negative values 382 | - `0` for `0??` 383 | - `slice??` will return a number from an attribute-based range 384 | - `string??` will return a random character 385 | - `table??` will return a random key 386 | - Shebang support 387 | - Special method for functions: 388 | - `function$` will now return the number of parameters the function accepts 389 | - While loop condition is now optional (`.. {}` is equivalent to `.. / {}`) 390 | 391 | ### Changed 392 | - `<>` now serves for setting the default parameter value 393 | - Bumped the minimum Python version to 3.9 394 | - Classes no longer need the `create` method defined to be initializable. 395 | - Improved scopes and empty body handling 396 | - Improved and added new error messages, such as: 397 | - Invalid table keys 398 | - Unbalanced brackets 399 | - Too many parameters for the main function (or the lack of one) 400 | - Improved object speed: 401 | - `Array`s — up to 30% faster 402 | - `Integer`s — up to 2.4x faster 403 | - `Null` objects — up to 2.2x faster 404 | - `String`s — up to 40% faster 405 | - `Table`s — up to 25% faster 406 | - Improved transpiling safety 407 | - Improved internal typechecking accuracy 408 | - RNG syntax (`^^x -> y^^`) was replaced by the new random `object??` syntax 409 | - `Slice`s can now be instantiated as standalone objects 410 | - Various refactorings 411 | 412 | ### Fixed 413 | - Fixed `-c` option not tokenizing some statements properly, such as: 414 | - statements starting with a table definition 415 | - statements ending with a `!` 416 | - Fixed `string.join` for empty delimiters 417 | - Fixed `string.pad` doubling when `string$` was equal to `length` 418 | - Fixed exception name categorization 419 | - Fixed `string.replace` 420 | - Functions defined inside class methods no longer need an instance 421 | - Fixed false positives for multiple type verification 422 | - Fixed Integer construction for cross-type comparison 423 | 424 | ### Removed 425 | - `->` operator support for assert (`#`) 426 | - Constants 427 | - Default parameter value in the function definition 428 | - `math.fromDecimal` - use `types.Integer(string)` instead 429 | - `random.choice` - use `iterable??` instead 430 | 431 | 432 | ## [0.1.0] - 2022-03-05 433 | 434 | Initial release 🚀 435 | 436 | [0.1.0]: https://github.com/samarium-lang/Samarium/releases/tag/0.1.0 437 | [0.2.0]: https://github.com/samarium-lang/Samarium/compare/0.1.0...0.2.0 438 | [0.2.1]: https://github.com/samarium-lang/Samarium/compare/0.2.0...0.2.1 439 | [0.2.2]: https://github.com/samarium-lang/Samarium/compare/0.2.1...0.2.2 440 | [0.2.3]: https://github.com/samarium-lang/Samarium/compare/0.2.2...0.2.3 441 | [0.3.0]: https://github.com/samarium-lang/Samarium/compare/0.2.3...0.3.0 442 | [0.3.1]: https://github.com/samarium-lang/Samarium/compare/0.3.0...0.3.1 443 | [0.4.0]: https://github.com/samarium-lang/Samarium/compare/0.3.1...0.4.0 444 | [0.5.0]: https://github.com/samarium-lang/Samarium/compare/0.4.0...0.5.0 445 | [0.5.1]: https://github.com/samarium-lang/Samarium/compare/0.5.0...0.5.1 446 | [0.5.2]: https://github.com/samarium-lang/Samarium/compare/0.5.1...0.5.2 447 | [0.5.3]: https://github.com/samarium-lang/Samarium/compare/0.5.2...0.5.3 448 | [0.6.0]: https://github.com/samarium-lang/Samarium/compare/0.5.3...0.6.0 449 | [0.6.1]: https://github.com/samarium-lang/Samarium/compare/0.6.0...0.6.1 450 | [0.6.2]: https://github.com/samarium-lang/Samarium/compare/0.6.1...0.6.2 451 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021–2024 trag1c 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Samarium 2 | 3 | Samarium is a dynamic interpreted language transpiled to Python. 4 | Samarium, in its most basic form, doesn't use any digits or letters. 5 | 6 | Here's a `Hello, World!` program written in Samarium: 7 | 8 | ```kt 9 | "Hello, World!"!; 10 | ``` 11 | 12 | Documentation on how to program in Samarium can be found [here](https://samarium-lang.github.io/Samarium/). 13 | 14 | ## [Examples](https://samarium-lang.github.io/Samarium/latest/examples/) 15 | 16 | 17 | # Installation 18 | 19 | ## [pip](https://pypi.org/project/pip/) 20 | 21 | ```sh 22 | pip install samarium 23 | ``` 24 | 25 | ## [AUR](https://aur.archlinux.org/) 26 | 27 | ```sh 28 | git clone https://aur.archlinux.org/samarium.git && cd samarium && makepkg -sirc 29 | ``` 30 | or use your favorite [AUR helper](https://wiki.archlinux.org/title/AUR_helpers). 31 | 32 | ## Using Samarium 33 | 34 | You can run Samarium programs with `samarium program.sm`. 35 | `samarium-debug` may be used instead, which will first print out the intermediary Python code that the Samarium program is transpiled into, before executing it. 36 | 37 | Short | Long | Description 38 | :---: | :---: | :--- 39 | `-c ` | `--command ` | Can be used to execute Samarium code from the string `cmd`,
directly in the terminal. `cmd` can be one or more statements
separated by semicolons as usual. Note that the last statement
of `cmd` will be printed if it does not end in a semicolon. 40 | `-h` | `--help` | Shows the help message 41 | `-v` | `--version` | Prints Samarium version 42 | 43 | 44 | There is also a VSCode syntax highlighting extension for Samarium, which can be found [here](https://marketplace.visualstudio.com/items?itemName=Samarium.samarium-language). The source code can be found [here](https://github.com/samarium-lang/vscode-samarium). 45 | 46 | 47 | # Credits 48 | 49 | Samarium was inspired by several languages, including [brainfuck](https://esolangs.org/wiki/Brainfuck), [Rust](https://www.rust-lang.org/), and [Python](https://www.python.org/). 50 | 51 | Special thanks to: 52 | 53 | - [tetraxile](https://github.com/tetraxile) for helping with design choices and writing the docs 54 | - [MithicSpirit](https://github.com/MithicSpirit) for making an AUR package for Samarium 55 | - [DarviL82](https://github.com/DarviL82) for fixing some issues 56 | - [Endercheif](https://github.com/Endercheif) for making the documentation look fancy, helping with design choices, and adding partial Python Interoperability 57 | 58 | If you have any questions, or would like to get in touch, join my [Discord server](https://discord.gg/C8QE5tVQEq)! 59 | -------------------------------------------------------------------------------- /docs/arrays.md: -------------------------------------------------------------------------------- 1 | # Arrays 2 | 3 | Arrays are defined using square brackets, with items separated by commas: 4 | 5 | ```sm 6 | [\, /\, //] 7 | ``` 8 | 9 | Arrays can be concatenated with the `+` operator: 10 | 11 | > `[/, /\] + [//]` is the same as `[/, /\, //]` 12 | 13 | To remove items from an array, the `-` operator can be used with either an index 14 | or a value. When removing by value, only the first instance of each value will 15 | be removed. Trying to remove an element not present in the array will result in 16 | an error. 17 | 18 | > `["a", "b", "c"] - /` gives `["a", "c"]` 19 | > `["a", "b", "c", "d"] - ["b", "d"]` gives `["a", "c"]` 20 | 21 | `--` can be used instead if we want to remove elements even if they're not 22 | present in the array: 23 | 24 | > `["a", "b", "c", "d"] - ["c", "d", "e"]` gives `["a", "b"]` 25 | 26 | Negating an array will return its copy with duplicates removed: 27 | 28 | > `-[/\\\, //, //, //\, /\\\, //, /\/]` gives `[/\\\, //, //\, /\/]` 29 | 30 | If we want to remove duplicates of just one value, we can use `---`: 31 | 32 | > `[/\\\, //, //, //\, /\\\, //, /\/] --- //` gives `[/\\\, //, //\, /\\\, /\/]` 33 | 34 | Set-like union, intersection, and symmetric difference can be obtained by using 35 | `|`, `&` and `^`, respectively: 36 | 37 | ```sm 38 | primes: [/\, //, /\/, ///, /\//, //\/]!; 39 | evens: [/\, /\\, //\, /\\\, /\/\, //\\]!; 40 | 41 | "|", primes | evens!; 42 | "&", primes & evens!; 43 | "^", primes ^ evens!; 44 | ``` 45 | ``` 46 | [2, 3, 5, 7, 11, 13] 47 | [2, 4, 6, 8, 10, 12] 48 | | [2, 3, 5, 7, 11, 13, 4, 6, 8, 10, 12] 49 | & [2] 50 | ^ [3, 5, 7, 11, 13, 4, 6, 8, 10, 12] 51 | ``` 52 | 53 | Arrays of integers can be cast to type String: 54 | 55 | > `[//\/\\\, //\/\\/, ////\\/, //\\\\/]%` gives `"hiya"` -------------------------------------------------------------------------------- /docs/assets/icon-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samarium-lang/Samarium/c6c5c6d0aabf434efe7f80bdccc42c234b3f8228/docs/assets/icon-black.png -------------------------------------------------------------------------------- /docs/assets/icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samarium-lang/Samarium/c6c5c6d0aabf434efe7f80bdccc42c234b3f8228/docs/assets/icon-white.png -------------------------------------------------------------------------------- /docs/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samarium-lang/Samarium/c6c5c6d0aabf434efe7f80bdccc42c234b3f8228/docs/assets/icon.png -------------------------------------------------------------------------------- /docs/assets/script.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const codeBlocks = document.querySelectorAll("pre.sm"); 3 | 4 | fetch("https://raw.githubusercontent.com/samarium-lang/vscode-samarium/master/syntaxes/samarium.tmLanguage.json") 5 | .then((data) => data.json()) 6 | .then((grammar) => ({ 7 | id: "samarium", 8 | scopeName: "source.samarium", 9 | grammar, 10 | })) 11 | .then(async (sm) => shiki.getHighlighter({ theme: "github-dark", langs: [sm] })) 12 | .then((h) => { 13 | [...codeBlocks].forEach((block) => { 14 | block.innerHTML = highlight(h, block.innerText); 15 | }); 16 | }); 17 | 18 | function highlight(h, code) { 19 | return h.codeToHtml(code, { lang: "samarium" }); 20 | } 21 | -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | code { 2 | --md-code-bg-color: reset !important; 3 | } 4 | -------------------------------------------------------------------------------- /docs/builtins.md: -------------------------------------------------------------------------------- 1 | # Built-in Functions 2 | 3 | 4 | ## INPUT 5 | 6 | Standard input can be read from with `???`. 7 | It will read until it receives a newline character. 8 | 9 | `x: ???;` will assign to `x` as a string what it reads from standard input, 10 | stopping at a newline character. 11 | 12 | A prompt can be given by preceding the `???` with a string, 13 | for example `x: "input: "???;`. 14 | 15 | > Related library: [`io`](stdio.md) 16 | 17 | 18 | ## PRINTLN 19 | 20 | Objects can be written to standard output by adding an `!` at the end. 21 | Note that they won't be written exactly as they would appear in Samarium. 22 | 23 | `"a"!` will write `a` to standard output. 24 | 25 | `//\/!` will write `13` to standard output. 26 | 27 | This function will return what it writes to stdout (though not necessarily as 28 | a string), and can thus be used in an assignment statement for example. 29 | 30 | ```sm 31 | x: //\! == optional semicolon after ! 32 | == the string "6" is written to stdout, and `x` now has the value 6 (number) 33 | ``` 34 | 35 | 36 | ## THROW 37 | 38 | Similarly to PRINTLN, some objects can be written to standard error using `!!!`. 39 | This will throw an error, and exit the program if the error is not caught. 40 | Only strings and arrays of length 2 can be used for throwing errors. 41 | 42 | ```sm 43 | "exception raised"!!! == optional semicolon after !!! 44 | == the following is written to stderr: 45 | == [Error] exception raised 46 | 47 | == 2-element arrays can be used for adding notes to exceptions: 48 | ["invalid date format", "use YYYY-MM-DD format"]!!! 49 | == [Error] invalid date format 50 | == [Note] use YYYY-MM-DD format 51 | ``` 52 | 53 | 54 | ## EXIT 55 | 56 | The program may be exited with `=>!`. 57 | If a particular exit code is desired, it may be put after the exclamation mark: 58 | 59 | `=>! //` will exit the program with exit code 3. 60 | 61 | A string may also be supplied instead of a number, which will first write it to stderr 62 | and then exit the program with exit code 1. 63 | 64 | > `=>! "uh oh";` is equivalent to `"uh oh\n" ~> /\; =>! /;` 65 | 66 | 67 | ## HASH 68 | 69 | The hash function `##` returns as a integer the hash value of an object if it 70 | has one (arrays and tables do not). The value it returns will remain consistent 71 | for the life of the program, but may vary if the program is run multiple times. 72 | 73 | `"hash"##` will return the hash value of the string `"hash"`. 74 | 75 | 76 | ## TYPEOF 77 | 78 | The typeof function `?!` returns the type of an object, 79 | as an instance of the `Type` class. 80 | 81 | > `/?!` returns `Number`. 82 | > `"abc"?!` returns `String`. 83 | > `/?!?!` returns `Type`. 84 | 85 | These instances are callable and can be used to convert values into that type, 86 | like so: 87 | 88 | ```sm 89 | \?!("123") + /\!; 90 | == writes `125` to stdout 91 | ``` 92 | 93 | > Related library: [`types`](stdtypes.md) 94 | 95 | 96 | ## SPECIAL 97 | 98 | The special method `$` has different uses depending on the type of object it's 99 | used on. 100 | 101 | Object | Use 102 | --- | --- 103 | Array | Returns the length of the array 104 | Function | Returns the number of parameters the function has 105 | Number | Returns the integer part of the number as a string 106 | Slice | Returns the length of the range covered by the slice 107 | String | Returns the length of the string 108 | Table | Returns an array of the table's values 109 | 110 | For example: 111 | 112 | ```sm 113 | "string"$!; 114 | == writes `6` to stdout 115 | ``` 116 | 117 | 118 | ## TIMESTAMP 119 | 120 | The timestamp function `@@` returns the time in milliseconds since the epoch. 121 | 122 | !!! note 123 | Timezone is included. For UTC timestamp, use `datetime.timestamp_utc`. 124 | 125 | > Related library: [`datetime`](stddatetime.md) 126 | 127 | 128 | ## DTNOW 129 | 130 | The dtnow function `@@@` gets the system's current date and time as an array of 131 | integers, in the format `[year, month, day, hour, minute, second, millisecond, 132 | utc_hour_offset, utc_minute_offset]`. 133 | 134 | > Related library: [`datetime`](stddatetime.md) 135 | 136 | ## SLEEP 137 | 138 | The sleep function `,.,` pauses execution for the specified number of 139 | milliseconds. 140 | 141 | ```sm 142 | ,., /////\/\\\; 143 | == sleep for 1000 milliseconds (1 second) 144 | ``` 145 | 146 | > Related library: [`datetime`](stddatetime.md) 147 | 148 | 149 | ## ASSERT 150 | 151 | The assert function `!!` is used as a debugging tool. 152 | If the input to this function is falsy (i.e. empty iterable, null, or just 153 | false), an `AssertionError` will be raised, otherwise, nothing will happen. 154 | A custom error message can be provided by putting it after a `,`. 155 | 156 | ```sm 157 | !! / > /\, "error message"; 158 | ``` 159 | 160 | will raise `[AssertionError] error message`. 161 | 162 | 163 | ## PARENT 164 | 165 | The parent function `!?` gets the parent/inherited classes of the input class. 166 | If a class only has one parent, it will be returned directly. 167 | If a class has multiple parents, they will be returned in an array, in the same 168 | order as in the class definition. 169 | 170 | Note that this will only go one layer deep, i.e. if class `A` has parent `B` 171 | and class `B` has parent `C`, `A!?` will only return `B`, and `A!?!?` will 172 | return `C`. 173 | 174 | 175 | ## RANDOM 176 | 177 | The special method `??` has different uses depending on the type of object it's 178 | used on. 179 | 180 | Object | Use 181 | --- | --- 182 | Array | Returns a random element 183 | Number | Returns a random number from
an interval based on its value
`n = 0` → `0`
`n > 0` → `[0, n)`
`n < 0` → `[n, 0)`
If `n` is an integer, `n??` is also going to return integers. 184 | Slice | Returns a random number in a range defined by that slice 185 | String | Returns a random character 186 | Table | Returns a random key 187 | 188 | > Related library: [`random`](stdrandom.md) 189 | 190 | 191 | ## ID 192 | 193 | The special method `**` returns the memory address of a given object. 194 | 195 | ```sm 196 | x: //; 197 | x**!; == 7f7ea0367800 198 | ``` 199 | -------------------------------------------------------------------------------- /docs/classes.md: -------------------------------------------------------------------------------- 1 | # Classes 2 | 3 | Classes are defined with the `@` character. Any inherited classes can follow the 4 | name of the class in parentheses, separated by commas[^1]. 5 | Class variables and methods are defined inside curly braces. 6 | 7 | Inside a class' methods, the `'` character can be used to reference its instance 8 | variables and methods. Class variables are those that are shared between all 9 | instances of a class, and instance variables are for data unique to each 10 | instance. 11 | 12 | Class variables, instance variables and methods can be accessed with the `.` 13 | operator on an instance of the class. In this case, class methods will 14 | implicitly be given a reference to the instance as the first argument of the 15 | method; `x.method(arg1, arg2, ...)` is equivalent to `x?!.method(x, arg1, arg2, ...)`. 16 | Class variables and methods can also be accessed in the same way directly from 17 | the class itself, though note that in this case methods will not implicitly have 18 | an instance as the first argument, so it must be provided. 19 | 20 | Just like variables, class attributes can be made private by prefixing them with 21 | `#`, making them inaccessible outside the class. 22 | 23 | ```sm 24 | @ Foo { 25 | shared: []; 26 | 27 | => var * { 28 | 'var: var; 29 | '#pv: var - /; 30 | } 31 | 32 | get_pv * { 33 | * '#pv; 34 | } 35 | } 36 | 37 | a: Foo(/\\); == calls `Foo.create`; `a` is now an instance of `Foo` 38 | a.var!; == prints 5 39 | a.get_pv()!; == calls `Foo.get_pv(a)`; prints 4 40 | 41 | b: Foo(/\); 42 | a.shared+: ["str"]; == modifying a class variable for all instances 43 | b.shared!; == prints ["str"] 44 | ``` 45 | 46 | Parent classes are inherited from right to left, i.e. the first class in the 47 | inheritance takes priority and will overwrite any functions/variables defined 48 | by the following classes: 49 | 50 | ```sm 51 | @ A { 52 | method * { "A"!; } 53 | } 54 | 55 | @ B { 56 | method * { "B"!; } 57 | } 58 | 59 | @ C(A, B) {} 60 | 61 | => * { 62 | c: C(); 63 | c.method(); == prints "A", as class A was inherited last 64 | } 65 | ``` 66 | 67 | 68 | There are a number of special methods that the user can implement to override 69 | certain functionality of a class, such as how it's initialized (with the 70 | `create` method), or how it interacts with different operators. These methods 71 | are as follows (where `func(...)` indicates a variable number of arguments): 72 | 73 |
74 | 75 | Function | Python | Use 76 | --- | --- | --- 77 | `+(other)` | `add` | Interacts with the addition operator `+`. 78 | `&(other)` | `and` | Interacts with the bitwise AND operator `&`. 79 | `()(...)` | `call` | Called when an instance itself is "called" as a function;
`x(...)` roughly translates to `x?!.call(x, ...)`. 80 | `%()` | -- | Interacts with the cast function operator `%`. 81 | `=>(...)` | `init` | Initializes an instance of a class, takes any number
of arguments. Typically used for setting instance
variables based on these arguments.
No return value necessary. 82 | `--(other)` | `floordiv` | Interacts with the division operator `--`. 83 | `::(other)` | `eq` | Implements the equality operator `::`. 84 | `<<>>(index)` | `getitem` | Implements indexing an object;
`x<>` is equivalent to `x.get_item(index)`. 85 | `>(other)` | `gt` | Implements the greater than operator `>`. 86 | `>:(other)` | `ge` | Implements the greater than or equal operator `>:`. 87 | `->?(item)` | `contains` | Implements membership testing,
returns `1` (object contains `item`)
or `0` (object does not contain `item`).
Interacts with `->?` operator. 88 | `##()` | `hash` | Called by the built-in hash function `##`,
and for keys in a table.
Objects which compare equal
should have the same hash value. 89 | `...()` | `iter` | Called when iterating over an object in a `foreach` loop.
Returns an array of objects to iterate over. 90 | `<(other)` | `lt` | Implements the less than operator `<`. 91 | `<:(other)` | `le` | Implements the less than or equal operator `<:`. 92 | `---(other)` | `mod` | Interacts with the modulo operator `---`. 93 | `++(other)` | `mul` | Interacts with the multiplication operator `++`. 94 | `-_()` | `neg` | Interacts with the negative unary operator `-`. 95 | `~()` | `invert` | Interacts with the bitwise NOT operator `~`. 96 | `:::(other)` | `ne` | Implements the inequality operator `:::`. 97 | `|(other)` | `or` | Interacts with the bitwise OR operator `\|`. 98 | `+_()` | `pos` | Interacts with the positive unary operator `+`. 99 | `+++(other)` | `pow` | Interacts with the exponentiation operator `+++`. 100 | `??()` | -- | Interacts with the random function operator `??`. 101 | `<<>>:(index, value)` | `setitem` | Implements assigning to an index of an object;
`x<>: value` is equivalent
to `x.set_item(index, value)`. 102 | `$()` | -- | Interacts with the special function operator `$`. 103 | `-(other)` | `sub` | Interacts with the subtraction operator `-`. 104 | `?()` | `bool` | Implements boolean value testing,
returns `1` (truthy) or `0` (falsy).
Used for conditional statements and logical operators. 105 | `!()` | `str` | Returns the string representation of an object. 106 | `^(other)` | `xor` | Interacts with the bitwise XOR operator `^`. 107 | 108 |
109 | 110 | Two special methods – `=>` and `!` – have default definitions: 111 | ```sm 112 | @ Foo {} 113 | 114 | f: Foo(); 115 | f!; == 116 | ``` 117 | The above class definition is equivalent to: 118 | ```sm 119 | @ Foo { 120 | => * {} 121 | 122 | ! * { 123 | * "<$name@$address>" --- {{"name" -> '?!, "address" -> '**}}; 124 | } 125 | } 126 | ``` 127 | 128 | Some of the comparison operators can be inferred from others, 129 | so not all of them are necessary to provide implementations for. 130 | The following operators infer from each other: 131 | - `::` and `:::` 132 | - `>` and `<` 133 | - `>:` and `<:` 134 | 135 | 136 | ## Static Methods 137 | Methods can be made static by replacing the `*` keyword with the `~'*` keyword 138 | (where `~'` can be read as "no instance"): 139 | ```sm 140 | <=calendar.date; 141 | 142 | @ Calendar { 143 | is_weekend date ~'* { 144 | * date.weekday > /\\; 145 | } 146 | } 147 | 148 | Calendar.is_weekend(date("2022-11-08"))!; == 0 149 | ``` 150 | 151 | 152 | ## Classes As Entry Points 153 | 154 | A class named `=>` can serve as an entry point instead of a function: 155 | ```sm 156 | => argv * { 157 | "Hello, " + argv<>!; 158 | } 159 | ``` 160 | ```sm 161 | @ => { 162 | => argv * { 163 | "Hello, " + argv<>!; 164 | } 165 | } 166 | ``` 167 | 168 | 169 | ## Class Decorators 170 | 171 | Decorators can also be created using classes: 172 | ```sm 173 | @ OutputStorage { 174 | 175 | => func * { 176 | 'func: func; 177 | 'outputs: []; 178 | } 179 | 180 | () args... * { 181 | out: 'function(**args); 182 | 'outputs_: [out]; 183 | * out; 184 | } 185 | 186 | } 187 | 188 | OutputStorage @ multiply a b * { 189 | * a ++ b; 190 | } 191 | 192 | multiply(/\, /\/)!; == 10 193 | multiply(//, ///)!; == 21 194 | multiply(/\\/, //\\)!; == 108 195 | 196 | multiply.outputs!; == [10, 21, 108] 197 | ``` 198 | 199 | [^1]: Note that order will be preserved here — if both class `A` and class `B` 200 | implement a function `f`, and class `C` inherits them in the order `(A, B)`, 201 | then `C` will inherit `f` from class `A`, as it is inherited later. 202 | 203 | 204 | ## Data Classes 205 | 206 | Samarium has a shorthand syntax for defining classes whose main purpose is 207 | storing data: 208 | ```sm 209 | @ Person { 210 | => name age * { 211 | 'name: name; 212 | 'age: age; 213 | } 214 | 215 | ! * { * "$0($1, $2)" --- ['?!, 'name, 'age]; } 216 | :: other * { * ['name, 'age] :: [other.name, other.age]; } 217 | > other * { * ['name, 'age] > [other.name, other.age]; } 218 | >: other * { * ['name, 'age] >: [other.name, other.age]; } 219 | } 220 | ``` 221 | The above definition can be replaced with just 222 | ```sm 223 | @! Person(name, age); 224 | ``` 225 | If we wish to define additional methods, we can just open a pair of braces after 226 | we specify the name and the fields like in a regular class: 227 | ```sm 228 | @! Person(name, age) { 229 | birthday * { 'age+:; } 230 | } 231 | 232 | p: Person("Jake", /\\//)! == Person("Jake", 19) 233 | p.birthday(); 234 | p! == Person("Jake", 20) 235 | ``` 236 | If you'd like your dataclass to not have any fields, you can either leave the 237 | parentheses empty or omit them entirely: 238 | ```sm 239 | @! Unit(); Unit()! == Unit() 240 | @! Unit; Unit()! == Unit() 241 | ``` 242 | 243 | By default, all dataclass instances have a special method defined which returns 244 | its copy: 245 | ```sm 246 | @! Person(name, age); 247 | 248 | p: Person("Bob", /\\//); 249 | q: p; 250 | r: p$; 251 | q.name: "Alice"; 252 | r.name: "Dave"; 253 | p, q, r! == Person("Alice", 19) Person("Alice", 19) Person("Dave", 19) 254 | ``` -------------------------------------------------------------------------------- /docs/comments.md: -------------------------------------------------------------------------------- 1 | # Comments 2 | 3 | Comments are written using `==`, and comment blocks are written with `==<` and 4 | `>==`: 5 | 6 | ```sm 7 | == single-line comment 8 | 9 | ==< comment block 10 | doesn't end 11 | on newlines >== 12 | ``` -------------------------------------------------------------------------------- /docs/controlflow.md: -------------------------------------------------------------------------------- 1 | # Control Flow 2 | 3 | 4 | ## `if`/`else` 5 | 6 | `if` statements are written using a `?` character, and `else` is written as 7 | `,,`. Blocks are enclosed in curly brackets. 8 | `else if` can be written using `,, ?`. 9 | 10 | ```sm 11 | ? x < \ { 12 | "x is negative"!; 13 | } ,, ? x > \ { 14 | "x is positive"!; 15 | } ,, { 16 | "x = 0"!; 17 | } 18 | ``` 19 | 20 | 21 | ## `foreach` loop 22 | 23 | `foreach` loops are written using `...`, and enclosed in curly brackets. 24 | They must be paired with a `->?` operator, indicating the object to iterate over. 25 | 26 | ```sm 27 | arr: []; 28 | ... char ->? "string" { 29 | arr+: [char]; 30 | } 31 | == arr :: ["s", "t", "r", "i", "n", "g"] 32 | ``` 33 | 34 | 35 | ### Comprehensions 36 | 37 | 38 | #### **Array Comprehensions** 39 | 40 | Array comprehensions are a way to create an array based on another iterable. 41 | Uses may include performing an operation on each item of the iterable, or 42 | creating a subsequence of those items that satisfy a certain condition. 43 | 44 | They are written similarly to `foreach` loops; they can come in two forms, 45 | as follows: 46 | 47 | ```sm 48 | [expression ... member ->? iterable] 49 | [expression ... member ->? iterable ? condition] 50 | ``` 51 | 52 | For example, say we want to create an array of square numbers. 53 | Here are two equivalent approaches: 54 | 55 | ```sm 56 | input: [/, /\, //, /\\, /\/]; 57 | 58 | == Approach 1 59 | arr: []; 60 | ... n ->? input { 61 | arr+: [n ++ n]; 62 | } 63 | 64 | == Approach 2 65 | arr: [n ++ n ... n ->? input]; 66 | ``` 67 | 68 | In both cases, `arr` is equal to `[1, 4, 9, 16, 25]`. 69 | 70 | Now suppose we want to filter this result to only the odd-numbered items. 71 | There are again two equivalent approaches: 72 | 73 | ```sm 74 | arr: [/, /\\, /\\/, /\\\\, //\\/]; 75 | 76 | == Approach 1 77 | filtered: []; 78 | ... n ->? arr { 79 | ? n --- /\ :: / { 80 | filtered+: [n]; 81 | } 82 | } 83 | 84 | == Approach 2 85 | filtered: [n ... n ->? arr ? n --- /\ :: /]; 86 | ``` 87 | 88 | In both cases, `filtered` is equal to `[1, 9, 25]`. 89 | 90 | 91 | #### **Table Comprehensions** 92 | 93 | Table comprehensions have a similar syntax to array comprehensions: 94 | 95 | ```sm 96 | {{key -> value ... member ->? iterable}} 97 | {{key -> value ... member ->? iterable ? condition}} 98 | ``` 99 | 100 | For example, both of the following approaches are equivalent: 101 | 102 | ```sm 103 | == Approach 1 104 | tab: {{}}; 105 | ... x ->? [/\, /\\, //\] { 106 | tab<>: x ++ x; 107 | } 108 | 109 | == Approach 2 110 | tab: {{x -> x ++ x ... x ->? [/\, /\\, //\]}}; 111 | ``` 112 | 113 | In both cases, `tab` is equal to `{{2 -> 4, 4 -> 16, 6 -> 36}}`. 114 | 115 | 116 | ## `while` loop 117 | 118 | `while` loops are written with `..`, and enclosed in curly brackets. 119 | The loop condition follows the `..`. 120 | An infinite loop is created when no condition is given. 121 | 122 | ```sm 123 | x: \; 124 | .. x < /\/\ { 125 | x+: /\; 126 | x!; 127 | } 128 | == prints 2, 4, 6, 8, 10 129 | ``` 130 | 131 | 132 | ## `break`/`continue` 133 | 134 | `break` statements are written with `<-`, and terminate the enclosing loop 135 | immediately. They can be used in both `foreach` and `while` loops. 136 | 137 | ```sm 138 | x: \; 139 | .. x < /\/ { 140 | x+: /; 141 | ? x :: // { <- } 142 | x!; 143 | } 144 | ``` 145 | 146 | This program will print 1, 2, and then terminate the `while` loop on the third 147 | iteration, before printing 3. 148 | 149 | `continue` statements are written with `->`, and immediately finish the current 150 | iteration of the enclosing loop. These can also be used in both `for` and 151 | `while` loops. 152 | 153 | ```sm 154 | x: \; 155 | .. x < /\/ { 156 | x+: /; 157 | ? x :: // { -> } 158 | x!; 159 | } 160 | ``` 161 | 162 | This program will print 1, 2, skip the third iteration of the `while` loop, 163 | then print 4, 5, and end the loop normally. 164 | 165 | !!! note 166 | Both `<-` and `->` do not need a semicolon at the end of the statement 167 | – it's optional. 168 | 169 | 170 | ## `try`/`catch` 171 | 172 | `try`-`catch` statements are used for error handling. 173 | `try` clauses are written with `??`, and enclosed in curly brackets. 174 | If, during execution of the contents of the `try` clause, an error is thrown, 175 | the rest of the clause is skipped, the error will be silenced, and the adjoining 176 | `catch` clause will be executed. `catch` clauses are written with `!!`, and are 177 | also enclosed in curly brackets. 178 | 179 | ```sm 180 | ?? { 181 | == error prone code here... 182 | / -- \; 183 | "unreachable"!; 184 | } !! { 185 | "error caught"!; 186 | } 187 | ``` -------------------------------------------------------------------------------- /docs/enums.md: -------------------------------------------------------------------------------- 1 | # Enums 2 | 3 | Enums are defined using the `#` character, before which comes the name of the 4 | enum. 5 | 6 | Each of the enum members has to be separated with a semicolon. 7 | 8 | By default, enum members are assigned increasing numbers, starting from 0. 9 | 10 | You can provide your own values for enum members by simply assigning values to 11 | the names. 12 | 13 | Enum members cannot be modified. Enums are truthy when they have at least 1 14 | member. 15 | 16 | ```sm 17 | Shape # { 18 | Circle; 19 | Square; 20 | } 21 | 22 | Color # { 23 | Red: "#FF0000"; 24 | Green: "#00FF00"; 25 | Blue: "#0000FF"; 26 | } 27 | 28 | Shape!; 29 | Shape.Circle!; 30 | Shape.Square!; 31 | Color.Red!; 32 | ``` 33 | ``` 34 | Enum(Shape) 35 | 0 36 | 1 37 | #FF0000 38 | ``` 39 | 40 | Enums can be casted to Tables: 41 | ```sm 42 | Shape # { 43 | Circle; 44 | Square; 45 | } 46 | 47 | Color # { 48 | Red: "#FF0000"; 49 | Green: "#00FF00"; 50 | Blue: "#0000FF"; 51 | } 52 | 53 | Shape%!; 54 | Color%!; 55 | ``` 56 | ``` 57 | {{"Circle" -> 0, "Square" -> 1}} 58 | {{"Red" -> "#FF0000", "Green" -> "#00FF00", "Blue" -> "#0000FF"}} 59 | ``` -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | 4 | ## Reverse a file's contents 5 | 6 | ```sm 7 | => argv * { 8 | file: argv<>; 9 | data <~ file; 10 | -data ~> file; 11 | } 12 | ``` 13 | 14 | 15 | ## Counting duplicates 16 | 17 | ```sm 18 | arr: <-io.inputcast("Enter an array of values: "); 19 | "Found $0 duplicate(s) in the array" --- [arr$ - (-arr)$]!; 20 | ``` 21 | 22 | 23 | ## Checking Pythagorean triplets 24 | 25 | ```sm 26 | a, b, c: <-iter.sorted(<-io.inputcast("Enter three integers: ")); 27 | "These numbers do $0form a Pythagorean triplet" 28 | --- ["not " ++ (~~(a+++ + b+++ :: c+++))]!; 29 | ``` 30 | 31 | 32 | ## Generating a password 33 | 34 | ```sm 35 | CHARSET: <-string.LETTERS + <-string.DIGITS; 36 | ... c ->? [CHARSET?? ... _ ->? <<../?!("Password length: "???)>>] { c ~> /; }!; 37 | ``` 38 | 39 | 40 | ## Factorial function 41 | 42 | ```sm 43 | factorial n * { 44 | !! n>:, "n cannot be negative"; 45 | ? ~~ n { * /; } 46 | * n ++ factorial(n-); 47 | } 48 | 49 | n: /?!("n: "???); 50 | "n! =", factorial(n)!; 51 | ``` 52 | 53 | 54 | ## Memoization 55 | 56 | ```sm 57 | <=types.Frozen; 58 | 59 | memoize func * { 60 | cache: {{}}; 61 | wrapper args... * { 62 | args: Frozen(args); 63 | ? args ->? cache { * cache<>; } 64 | result: func(**args); 65 | cache<>: result; 66 | * result; 67 | } 68 | * wrapper; 69 | } 70 | 71 | memoize @ fib n * { 72 | ? n < /\ { * n; } 73 | * fib(n-) + fib(n - /\); 74 | } 75 | 76 | fib(///\/)!; 77 | ``` 78 | 79 | 80 | ## Filtering and enumerating a file 81 | 82 | ```sm 83 | == Reads URLs from a file, enumerates them and prints them out. 84 | == If a URL is 32 characters or longer, it will be printed in red. 85 | <=string.[to_upper, strip]; 86 | 87 | urls <~~ "urls.txt"; 88 | ... lineno, line ->? <> >< urls { 89 | line: strip(line); 90 | ? line$ > ///// { 91 | line: "\033[31m$0\033[0m" --- line; 92 | } 93 | "$0. $1" --- [lineno, line]!; 94 | } 95 | ~urls; 96 | ``` 97 | 98 | 99 | ## Guess the number game 100 | 101 | ```sm 102 | "Guess the number!"!; 103 | secret_number: (/\/\+++)??+; 104 | 105 | .. { 106 | guess: "Please input your guess: "???; 107 | 108 | ?? { guess: /?!(guess); } 109 | !! { -> } 110 | 111 | "You guessed:", guess!; 112 | 113 | ? guess < secret_number { "Too small!"!; -> } 114 | ? guess > secret_number { "Too big!"!; -> } 115 | 116 | "You win!"!; 117 | <- 118 | } 119 | ``` 120 | 121 | 122 | ## Point class implementation 123 | 124 | ```sm 125 | <=operator -> op; 126 | <=iter.map; 127 | 128 | @ Point { 129 | => scalars... * { 'vector: scalars; } 130 | ! * { * "($0)" --- <-string.join('vector, ", "); } 131 | ... * { ... i -.? 'vector { **i; } } 132 | magnitude * { * <-math.sum('vector) +++ `/; } 133 | 134 | - other * { * Point(**map(op.sub, ' >< other)); } 135 | + other * { * Point(**map(op.add, ' >< other)); } 136 | -- other * { * Point(**map(op.div, ' >< other)); } 137 | ++ other * { * Point(**map(op.mul, ' >< other)); } 138 | --- other * { * Point(**map(op.mod, ' >< other)); } 139 | >< other * { * 'vector >< other.vector; } 140 | +++ other * { 141 | out: []; 142 | ... a ->? 'vector { 143 | ... b ->? other.vector { out+: [[a, b]]; } 144 | } 145 | * -out; 146 | } 147 | } 148 | 149 | q, p: Point(/\, //, /\\), Point(//, /\\, /\/); 150 | 151 | p.magnitude()!; == 3.4641016151377544 152 | []?!(q)!; == [2, 3, 4] 153 | 154 | p + q!; == (5, 7, 9) 155 | p - q!; == (1, 1, 1) 156 | p ++ q!; == (6, 12, 20) 157 | p --- q!; == (1, 1, 1) 158 | p +++ q!; 159 | == [ 160 | == [3, 2], [3, 3], [3, 4], 161 | == [4, 2], [4, 3], [4, 4], 162 | == [5, 2], [5, 3], [5, 4] 163 | == ] 164 | ``` 165 | 166 | 167 | ## Optional arguments 168 | 169 | ```sm 170 | rect_area length width? * { 171 | width <> length; 172 | * width ++ length; 173 | } 174 | 175 | rect_area(/\/, /\\\)!; == 40 176 | rect_area(/\/)!; == 25 177 | ``` 178 | -------------------------------------------------------------------------------- /docs/fileio.md: -------------------------------------------------------------------------------- 1 | # File I/O 2 | 3 | Files are handled through file I/O objects, which can be in one of several 4 | modes: read, write, read & write, append, and as either text or binary for each 5 | of these. File I/O objects have a cursor, which is updated whenever data is 6 | written to/read from the object. File objects are truthy when they are open. 7 | The current cursor position can be gotten like so: 8 | 9 | ```sm 10 | pos: f<<>>; 11 | == assuming `f` is a file I/O object 12 | ``` 13 | 14 | 15 | ## Creating 16 | 17 | Files can be created with the unary `?~>` operator. 18 | `?~> "file.txt"` will create an empty file called `file.txt` in the program 19 | directory. 20 | 21 | !!! note 22 | Files will also be created if they are opened in write or append mode. 23 | 24 | 25 | ## Reading 26 | 27 | Files can be opened for reading in two ways: 28 | 29 | ```sm 30 | f <~~ "file.txt"; 31 | == opens `file.txt` for reading, in text mode, 32 | == and stores the file I/O object in `f`. 33 | 34 | f <~% "file.bin"; 35 | == opens `file.bin` for reading, in binary mode, 36 | == and stores the file I/O object in `f`. 37 | ``` 38 | 39 | These file I/O objects can be read into a variable (a string for text mode, and 40 | an array of integers for binary mode) for use in the program. 41 | 42 | ```sm 43 | string <~ f; 44 | == reads the full contents of the file I/O object `f` 45 | == into `string` (assuming `f` is in text read mode) 46 | 47 | array <% f; 48 | == reads the full contents of the file I/O object `f` 49 | == into `array` (assuming `f` is in binary read mode) 50 | ``` 51 | 52 | File objects are also iterable, yielding one line per iteration: 53 | ```sm 54 | path: "hello_world.rs"; 55 | f <~~ path; 56 | 57 | "File:", path!; 58 | "-" ++ (//\ + path$)!; 59 | ... lineno, line ->? <> >< f { 60 | lineno, "|", <-string.strip(line, "\n")!; 61 | } 62 | 63 | ~f; 64 | ``` 65 | ``` 66 | File: hello_world.rs 67 | -------------------- 68 | 1 | fn main() { 69 | 2 | println!("Hello, World!"); 70 | 3 | } 71 | ``` 72 | 73 | 74 | ## Writing 75 | 76 | Files can be opened for writing in two ways: 77 | 78 | ```sm 79 | f ~~> "file.txt"; 80 | == opens/creates `file.txt` for writing, in text 81 | == mode, and stores the file I/O object in `f`. 82 | 83 | f %~> "file.bin"; 84 | == opens/creates `file.bin` for writing, in binary 85 | == mode, and stores the file I/O object in `f`. 86 | ``` 87 | 88 | These file I/O objects can be written to from a variable (a string for text 89 | mode, and an array of integers for binary mode). 90 | 91 | ```sm 92 | string ~> f; 93 | == writes the entirety of `string` into the file I/O 94 | == object `f` (assuming `f` is in text write mode) 95 | 96 | string %> f; 97 | == writes the entire contents of `array` into the file I/O 98 | == object `f` (assuming `f` is in binary write mode) 99 | ``` 100 | 101 | 102 | ## Appending 103 | 104 | Files can be opened for appending in two ways: 105 | 106 | ```sm 107 | f &~~> "file.txt"; 108 | == opens/creates `file.txt` for appending, in text 109 | == mode, and stores the file I/O object in `f`. 110 | 111 | f &%~> "file.bin"; 112 | == opens/creates `file.bin` for appending, in binary 113 | == mode, and stores the file I/O object in `f`. 114 | ``` 115 | 116 | The contents of these file I/O objects can be added to from a variable (a string 117 | for text mode, and an array of integers for binary mode). 118 | 119 | ```sm 120 | string &~> f; 121 | == appends the entirety of `string` to the current contents of 122 | == the file I/O object `f` (assuming `f` is in text append mode) 123 | 124 | array &%> f; 125 | == appends the entirety of `array` to the current contents of 126 | == the file I/O object `f` (assuming `f` is in binary append mode) 127 | ``` 128 | 129 | 130 | ## Closing 131 | 132 | Files can be closed with the `~` operator. 133 | If files are not closed manually by the user, they will be automatically closed 134 | once the program terminates. Note that the file I/O object will not be released 135 | from memory, but it still cannot be used. 136 | 137 | ```sm 138 | ~f; 139 | == closes the file I/O object `f` 140 | ``` 141 | 142 | 143 | ## Quick Operations 144 | 145 | Files can be read from, written to or appended to directly using the filename, 146 | with quick operations. These will open the file in the relevant mode, perform 147 | the operation, and close it, all in one. 148 | 149 | Mode | Operator 150 | --- | --- 151 | Text read | `<~` 152 | Text write | `~>` 153 | Text append | `&~>` 154 | Binary read | `<%` 155 | Binary write | `%>` 156 | Binary append | `&%>` 157 | 158 | For example: 159 | 160 | ```sm 161 | string ~> "file.txt"; 162 | == writes the entirety of `string` directly into `file.txt` 163 | 164 | array <% "file.bin"; 165 | == reads the full contents of `file.bin` directly into `array` 166 | ``` 167 | 168 | 169 | ## File Descriptors 170 | You can also use file descriptors instead of file paths in order to access 171 | standard I/O streams. 172 | 173 | Integer value | Name 174 | :---: | :---: 175 | `\` | Standard Input 176 | `/` | Standard Output 177 | `/\` | Standard Error 178 | 179 | An example use of these could be printing without a newline at the end: 180 | ```sm 181 | ... i ->? <<../\/\>> { 182 | i ~> /; 183 | } 184 | ``` 185 | The above code is equivalent to the following Python snippets: 186 | ```py 187 | def main(): 188 | for i in range(10): 189 | print(i, end="", flush=True) 190 | 191 | if __name__ == "__main__": 192 | main() 193 | ``` 194 | ```py 195 | import sys 196 | 197 | def main(): 198 | for i in range(10): 199 | sys.stdout.write(str(i)) 200 | sys.stdout.flush() 201 | 202 | if __name__ == "__main__": 203 | main() 204 | ``` 205 | All snippets produce the following output: 206 | ``` 207 | 0123456789 208 | ``` 209 | -------------------------------------------------------------------------------- /docs/functions.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | Functions are defined using the `*` character. 4 | Both the function's name and its parameters come before this `*` character, in 5 | that order, separated by spaces. 6 | The function body is enclosed in curly brackets. 7 | The function's return value is preceded by a `*` character as well. 8 | (Functions may also have multiple return statements, or none at all.) 9 | 10 | ```sm 11 | add a b * { 12 | sum: a + b; 13 | * sum; 14 | } 15 | ``` 16 | 17 | Calling a function is done as in C-like languages, with the function name, 18 | followed by its arguments in parentheses, separated by commas. 19 | 20 | ```sm 21 | a: /; 22 | b: /\; 23 | c: add(a, b); 24 | ``` 25 | 26 | If no value is returned, the semicolon after `*` may be omitted: 27 | 28 | ```sm 29 | exit code * { 30 | ? code {*} 31 | "Success"!; 32 | } 33 | ``` 34 | 35 | ## Main Function 36 | 37 | The main function/entrypoint of the program is denoted by `=>`. 38 | This function will be implicitly called on execution of the program. 39 | The return value of the main function indicates the exit code of the program 40 | (optional, defaults to 0). This function will not be called when importing 41 | the module it's located in. Command line arguments can be gotten as an array 42 | with an optional parameter in this function. 43 | 44 | ```sm 45 | => argv * { 46 | == program here 47 | } 48 | ``` 49 | 50 | 51 | ## Optional Parameters 52 | 53 | Parameters can be made optional by adding a `?` character after the parameter's 54 | name. Optional parameters are required to have a default value defined in the 55 | function's body using the `param <> default` syntax. 56 | 57 | ```sm 58 | == string.leftpad 59 | leftpad string length char? * { 60 | char <> " "; 61 | * pad(string, length, char) + string; 62 | } 63 | 64 | leftpad("hello", /\/\)!; == hello 65 | leftpad("hello", /\/\, "-")!; == -----hello 66 | ``` 67 | 68 | 69 | ## Varargs 70 | 71 | A function can accept a variable number of arguments by adding `...` after the 72 | last parameter's name. Packed arguments will be passed into the function as an 73 | array. 74 | 75 | ```sm 76 | product nums... * { 77 | prod: /; 78 | ... n ->? nums { 79 | prod++: n; 80 | } 81 | * prod; 82 | } 83 | 84 | prod()!; == 1 85 | prod(///)!; == 7 86 | prod(///, /\/\/)!; == 147 87 | prod(/\/, /\\\/\, /\, /\\/\\\, ///)!; == 171360 88 | ``` 89 | 90 | 91 | ## Argument Unpacking 92 | 93 | Arguments can be spread into a function by using the `**` unary operator: 94 | 95 | ```sm 96 | pow a b * { 97 | * a +++ b; 98 | } 99 | 100 | arguments = [/\, //]; 101 | 102 | pow(**arguments)!; 103 | == equivalent to pow(/\, //)!; 104 | ``` 105 | 106 | 107 | ## Decorators 108 | 109 | Decorators are syntactic sugar for calling a function/class which argument is 110 | another callable. 111 | 112 | To use a function as a decorator, write the name, `@` and then declare the 113 | function it is decorating. 114 | 115 | ```sm 116 | == Decorator 117 | double func * { 118 | wrapper args... * { 119 | * func(**args) ++ /\; 120 | } 121 | * wrapper; 122 | } 123 | 124 | == Decorated functions 125 | double @ multiply a b * { 126 | * a ++ b; 127 | } 128 | 129 | double @ code_to_char code * { 130 | * code%; 131 | } 132 | 133 | multiply(/\, /\\)!; == 16 134 | code_to_char(/\\\\/)!; == !! 135 | ``` 136 | 137 | 138 | ## Iterators 139 | 140 | Functions can yield values instead of returning them, thus making the function 141 | behave like an iterator. Values are yielded with the `**` operator: 142 | 143 | ```sm 144 | <=math.is_prime; 145 | 146 | prime_generator * { 147 | x: /\; 148 | .. { 149 | ? is_prime(x) { 150 | ** x; 151 | } 152 | x+: /; 153 | } 154 | } 155 | 156 | pg: prime_generator(); 157 | pg!; 158 | "Primes below 100:"!; 159 | ... i ->? pg { 160 | ? i > /\/\ +++ /\ { 161 | !; 162 | <- 163 | } 164 | ""?!(i) + " " ~> /; 165 | } 166 | ``` 167 | ``` 168 | 169 | Primes below 100: 170 | 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 171 | ``` 172 | 173 | Just like with `*`, the semicolon after `**` can be omitted if no value is 174 | yielded: 175 | ```sm 176 | foo * {**} 177 | 178 | []?!(foo())! == [null] 179 | ``` 180 | 181 | Iterators support special `$` and cast `%` methods. 182 | 183 | `Iterator%` returns the length of the iterator if available, and null otherwise. 184 | 185 | `Iterator$` yields the next value of the iterator. 186 | 187 | Iterators are always truthy. 188 | 189 | 190 | ## Function Composition 191 | 192 | Functions, types, and type aliases in Samarium can be composed together by using 193 | the `&` operator: 194 | 195 | ```sm 196 | (<-math.sqrt & <-operator.add)(//, //\)! == 3 197 | (<-types.Boolean & /?!)("1")! == true 198 | ``` 199 | ```sm 200 | <=math.[abs, max, min]; 201 | arrmap: []?! & <-iter.map; 202 | arrmap(abs, [/, `/, //\, -/\\, -/\/])! == [1, 0.5, 6, 4, 5] 203 | 204 | high x * { * min([x, /\/]); } 205 | low x * { * max([x, \]); } 206 | clamp: high & low; 207 | 208 | x: []?!(<<-//../\\/>>)! == [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8] 209 | arrmap(clamp, x)! == [0, 0, 0, 0, 1, 2, 3, 4, 5, 5, 5, 5] 210 | ``` 211 | ```sm 212 | compose funcs... * { 213 | * <-iter.reduce(<-operator.and, funcs); 214 | } 215 | 216 | repeat_function func times * { 217 | * compose(**[func]++times); 218 | } 219 | 220 | foo x * { 221 | out: x +++ /?!("0.1")! 222 | * out; 223 | } 224 | 225 | repeat_function(foo, /\/)(`/); 226 | == 0.9330329915368074 227 | == 0.9930924954370359 228 | == 0.9993070929904525 229 | == 0.9999306876841536 230 | == 0.999993068552217 231 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Samarium 2 | 3 | Samarium is a dynamic interpreted language transpiled to Python. 4 | Samarium, in its most basic form, doesn't use any digits or letters. 5 | 6 | Here's a `Hello, World!` program written in Samarium: 7 | 8 | ```sm 9 | "Hello, World!"!; 10 | ``` 11 | 12 | 13 | # Installation 14 | 15 | 16 | ## [pip](https://pypi.org/project/pip/) 17 | 18 | ```sh 19 | pip install samarium 20 | ``` 21 | 22 | 23 | ## [AUR](https://aur.archlinux.org/) 24 | 25 | ```sh 26 | git clone https://aur.archlinux.org/samarium.git && cd samarium && makepkg -sirc 27 | ``` 28 | or use your favorite [AUR helper](https://wiki.archlinux.org/title/AUR_helpers). 29 | 30 | 31 | ## Using Samarium 32 | 33 | You can run Samarium programs with `samarium program.sm`. 34 | `samarium-debug` may be used instead, which will first print out the 35 | intermediary Python code that the Samarium program is transpiled into, before 36 | executing it. 37 | 38 |
39 | 40 | Short | Long | Description 41 | :---: | :---: | :--- 42 | `-c ` | `--command ` | Can be used to execute Samarium code from the string `cmd`,
directly in the terminal. `cmd` can be one or more statements
separated by semicolons as usual. Note that the last statement
of `cmd` will be printed if it does not end in a semicolon. 43 | `-h` | `--help` | Shows the help message 44 | `-v` | `--version` | Prints Samarium version 45 | 46 |
47 | 48 | There is also a VSCode syntax highlighting extension for Samarium, which can be 49 | found [here](https://marketplace.visualstudio.com/items?itemName=Samarium.samarium-language). 50 | Its source code can be found [here](https://github.com/samarium-lang/vscode-samarium). 51 | 52 | 53 | # Credits 54 | 55 | Samarium was inspired by several languages, including 56 | [brainfuck](https://esolangs.org/wiki/Brainfuck), 57 | [Rust](https://www.rust-lang.org/), and [Python](https://www.python.org/). 58 | 59 | Special thanks to: 60 | 61 | - [tetraxile](https://github.com/tetraxile) for helping with design choices and 62 | writing the docs 63 | - [MithicSpirit](https://github.com/MithicSpirit) for making an AUR package for 64 | Samarium 65 | - [DarviL82](https://github.com/DarviL82) for fixing some issues 66 | - [Endercheif](https://github.com/Endercheif) for making the documentation look 67 | fancy, helping with design choices, and adding partial Python Interoperability 68 | 69 | If you have any questions, or would like to get in touch, join my [Discord server](https://discord.gg/C8QE5tVQEq)! 70 | -------------------------------------------------------------------------------- /docs/interop.md: -------------------------------------------------------------------------------- 1 | # Python Interoperability 2 | 3 | Samarium 0.4.0 introduced partial Python Interoperability, allowing you to use 4 | Python functions inside Samarium. 5 | 6 | The Python file you want to use has to be in the same directory as your Samarium 7 | file (so standard importing rules apply). 8 | 9 | Making a Python function usable in Samarium is as easy as decorating it with 10 | `@export`—it's gonna do all conversions between supported Samarium and Python 11 | types automatically. 12 | 13 | Python files are imported the same way as Samarium files. 14 | 15 | Samarium files take priority over Python files, meaning that if you have both 16 | `file.py` and `file.sm` in the same folder, Samarium will import `file.sm`. 17 | 18 | ## Examples 19 | 20 | ### Example 1 21 | ```py 22 | # foo.py 23 | from samarium import run, Registry 24 | from samarium.python import export 25 | 26 | 27 | @export 28 | def exec(code: str) -> None: 29 | run(code, Registry({}), __file__) 30 | ``` 31 | ```sm 32 | <-foo.exec("/////??!;"); == 14 33 | ``` 34 | 35 | ### Example 2 36 | ```py 37 | # bar.py 38 | import json 39 | 40 | from samarium.python import export 41 | 42 | 43 | @export 44 | def read_json(source: str) -> None: 45 | return json.loads(source) 46 | ``` 47 | ```sm 48 | source <~ "sample.json"; 49 | <-bar.read_json(source)!; 50 | == {{"hello" -> "world", "pi" -> 3.14}} 51 | ``` 52 | 53 | ## Supported Conversions 54 | 55 | ### Samarium → Python 56 | Samarium Type | Python Type 57 | --- | --- 58 | Array | list 59 | Enum | Enum 60 | File | IOBase 61 | Number | float 62 | Iterator | Iterator 63 | Null | NoneType 64 | Slice | SliceRange[^1] 65 | String | str 66 | Table | dict 67 | Zip | zip 68 | 69 | [^1]: 70 | 71 | ### Python → Samarium 72 | Python Type | Samarium Type 73 | --- | --- 74 | int | Number 75 | bool | Number 76 | float | Number 77 | str | String 78 | NoneType | Null 79 | list | Array 80 | tuple | Array 81 | set | Array 82 | dict | Table 83 | range | Slice 84 | slice | Slice 85 | SliceRange[^1] | Slice 86 | IOBase | File 87 | zip | Zip 88 | type[Enum] | Enum 89 | Iterator | Iterator 90 | 91 | Additionally, Enum members only get their values converted. 92 | 93 | [^1]: Samarium Slices can act as both Python `range`s and `slice`s, therefore a 94 | `SliceRange` object is being returned. It only has 2 properties, `range` and 95 | `slice`, which return exactly those objects. -------------------------------------------------------------------------------- /docs/modules.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | 3 | Modules can contain functions and variables that a user may wish to import. 4 | Modules are named after their filename (with `.sm` omitted). 5 | Like variables, module names must consist of only letters and numbers, and are 6 | case sensitive. 7 | 8 | 9 | ## Importing 10 | 11 | Modules can be imported using the `<-` operator, followed by the module's name. 12 | Objects (classes, functions, variables) from this module can then be accessed 13 | with the `.` operator. Module objects are always truthy. 14 | 15 | ```sm 16 | <=string; 17 | == imports the `string` module from Samarium's standard library 18 | 19 | string.to_upper("abc")!; 20 | == prints "ABC" 21 | 22 | string.digits!; 23 | == prints "0123456789" 24 | ``` 25 | 26 | Objects can also be directly imported from a module one by one, in which case 27 | they don't need to be preceded by the module name when using them: 28 | 29 | ```sm 30 | <=types.UUID4; 31 | <=math.[abs, sqrt]; 32 | 33 | UUID4()!; == a3ace080-8545-4852-a4d8-0385ccbb6a70 34 | sqrt(/\\/)!; == 3 35 | abs(-/\)!; == 2 36 | ``` 37 | 38 | All objects in a module can be directly imported at once by using the wildcard 39 | character `*`. Importing everything in this way is typically advised against, as 40 | it may cause poorly readable code and/or name collisions. 41 | 42 | ```sm 43 | <=math.*; 44 | 45 | factorial(//)!; == prints 6 46 | ``` 47 | 48 | 49 | ### Import Aliases 50 | 51 | Imported objects can be renamed if needed 52 | by using the `name -> new_name` syntax: 53 | ```sm 54 | <=string.[to_upper -> shout, strip]; 55 | 56 | str: " hello! "; 57 | shout(strip(str))!; == HELLO! 58 | ``` 59 | This is different to 60 | ``` 61 | <=string.to_upper; 62 | 63 | shout: to_upper; 64 | ``` 65 | because in this case, both `to_upper` and `shout` are valid options, 66 | whereas the first code block only has `shout`. 67 | 68 | 69 | ### Inline Imports 70 | 71 | Imports that are going to be only used once can be replaced 72 | by inline imports (using the `<-` keyword): 73 | ```sm 74 | i: /?!("Enter a number: "???); 75 | ? <-math.is_prime(i) { 76 | i, "is a prime number"!; 77 | } 78 | ``` -------------------------------------------------------------------------------- /docs/null.md: -------------------------------------------------------------------------------- 1 | # Null 2 | 3 | The null value in Samarium is not represented by any symbol—in fact, it's 4 | represented by the lack of it: 5 | ```sm 6 | x: /; 7 | y:; 8 | z: [-/, , /]; 9 | ``` 10 | The above code sets `x` to `1`, `y` to `null`, and `z` to `[-1, null, 1]`. 11 | 12 | The following code prints `1` if `y` is equal to `null` and `null` is present in 13 | `z`, and `0` otherwise. 14 | ```sm 15 | y :: && ->? z!; 16 | ``` 17 | One could alias null for clarity: 18 | ```sm 19 | null:; 20 | y :: null && null ->? z!; 21 | ``` 22 | 23 | Converting null to a number yields the value `0`. 24 | -------------------------------------------------------------------------------- /docs/numbers.md: -------------------------------------------------------------------------------- 1 | !!! note 2 | The following guide assumes that you are familiar with the basics of 3 | programming. 4 | 5 | # Numbers 6 | 7 | Numbers are represented in base 2, using slashes and backslashes to represent 1 8 | and 0 respectively. `` ` `` is used for the decimal point. 9 | Negative numbers are represented as normal, with a `-` sign before them. 10 | 11 | Let's see some examples of numbers: 12 | 13 | Base 10 | Base 2 | Samarium 14 | --- | --- | --- 15 | `0` | `0` | `\` 16 | `0.5` | `0.1` | ``\`/`` 17 | `1` | `1` | `/` 18 | `2` | `10` | `/\` 19 | `2.3125` | `10.0101` | ``/\`\/\/`` 20 | `3` | `11` | `//` 21 | `5` | `101` | `/\/` 22 | `8` | `1000` | `/\\\` 23 | `13` | `1101` | `//\/` 24 | `21` | `10101` | `/\/\/` 25 | 26 | Both the integer and decimal part of a number are optional, therefore: 27 | > ``/` `` is equivalent to ``/`\`` 28 | > `` `/`` is equivalent to ``\`/`` 29 | > `` ` `` is equivalent to ``\`\`` 30 | 31 | Integers can be cast to characters represented by that integer's unicode code 32 | point: 33 | 34 | > `/\\\\/%` returns `"!"` 35 | > `//////%` returns `"?"` -------------------------------------------------------------------------------- /docs/operators.md: -------------------------------------------------------------------------------- 1 | # Operators 2 | 3 | 4 | ## Arithmetic 5 | 6 | Operator | Meaning 7 | --- | --- 8 | `+` | Addition 9 | `-` | Subtraction 10 | `++` | Multiplication 11 | `--` | Division 12 | `+++` | Exponentiation 13 | `---` | Modulo 14 | 15 | 16 | ## Unary 17 | 18 | Operator | Meaning 19 | --- | --- 20 | `+` | Positive 21 | `-` | Negative 22 | 23 | 24 | ## Comparison 25 | 26 | Operator | Meaning 27 | --- | --- 28 | `<` | Less than 29 | `>` | Greater than 30 | `<:` | Less than or equal to 31 | `>:` | Greater than or equal to 32 | `::` | Equal to 33 | `:::` | Not equal to 34 | 35 | 36 | ## Logic and Membership 37 | 38 | Operator | Meaning 39 | --- | --- 40 | `&&` | Logical AND 41 | || | Logical OR 42 | `~~` | Logical NOT 43 | `->?` | `x ->? y` returns 1 if `x` is a member of `y`, and 0 if not 44 | 45 | 46 | ## Bitwise 47 | 48 | Operator | Meaning 49 | --- | --- 50 | `&` | Bitwise AND 51 | | | Bitwise OR 52 | `~` | Bitwise NOT 53 | `^` | Bitwise XOR 54 | 55 | 56 | ## Assignment 57 | 58 | All arithmetic and bitwise operators (except `~`) can be used together with the 59 | assignment operator. 60 | 61 | For example: 62 | 63 | ```sm 64 | x: x - /\/; 65 | x: x ++ //; 66 | x: x --- /\\; 67 | ``` 68 | 69 | is equivalent to: 70 | 71 | ```sm 72 | x-: /\/; 73 | x++: //; 74 | x---: /\\; 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/slices.md: -------------------------------------------------------------------------------- 1 | # Slices 2 | 3 | Slices are used to access a range of items in an iterable (strings, arrays, 4 | tables). They're enclosed in double angle brackets. 5 | Slices have three optional parameters delimited by `..` 6 | — `start`, `stop`, and `step`. 7 | 8 | Slices can be either applied to indexable objects (like `String` or `Array`) 9 | or serve as stand-alone objects. Slice objects are hashable. 10 | 11 | Iterating over a slice object generates integers from `start` (inclusive) 12 | to `stop` (exclusive) separated by gaps of size `step`. 13 | 14 | If `start` is not specified, it defaults to 0. 15 | If `stop` is not specified, it defaults to 263 - 1. 16 | If `step` is not specified, it defaults to 1. 17 | 18 | ```sm 19 | str: "abcdefgh"; 20 | str<<\>> :: "a"; 21 | str<> :: "def"; 22 | str<> :: "bdf"; 23 | ``` 24 | 25 | All valid slice parameters are as follows: 26 | 27 | Slice | Returns 28 | --- | --- 29 | `<<>>` | the whole iterable 30 | `<>` | the item at position `index` 31 | `<<..stop>>` | items up to index `stop` 32 | `<<....step>>` | items separated by gaps of size `step` 33 | `<>` | items starting from index `start` 34 | `<<..stop..step>>` | items up to index `stop` separated by gaps of size `step` 35 | `<>` | items starting from index `start` separated by gaps of size `step` 36 | `<>` | items starting from index `start` up to index `stop` 37 | `<>` | items starting from index `start` up to index `stop` separated by gaps of size `step` 38 | -------------------------------------------------------------------------------- /docs/stdcollections.md: -------------------------------------------------------------------------------- 1 | # `collections` module 2 | 3 | The `collections` module implements a few different data structure classes: 4 | [Stack](#stack), [Queue](#queue), [Set](#set), [Deque](#deque), 5 | and [ArithmeticArray](#arithmeticarray). 6 | 7 | 8 | ## Stack 9 | 10 | A stack is a collection of items that the user may "push" a new item on top of, 11 | or "pop" the most recently added/top item from, following a last in first out 12 | order (LIFO). 13 | 14 |
15 | 16 | Method                     | Use 17 | --- | --- 18 | `=>([size])` | Initializes an empty Stack object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the stack unbounded capacity. 19 | `is_empty()` | Returns `1` if the number of items in the stack is equal to 0, otherwise returns `0`. 20 | `is_full()` | Returns `1` if the number of items in the stack is equal to the specified capacity,
otherwise returns `0`.[^1] 21 | `peek()` | Returns the value of the item on top of the stack without popping it.
If the stack is empty, this will instead throw an error. 22 | `pop()` | Pops/removes an item from the top of the stack, and returns it.
If the stack is empty, this will instead throw an error. 23 | `push(item)` | Pushes `item` on top of the stack. If the stack is full,
i.e. its size is equal to the specified capacity, this will instead throw an error. 24 | `push_all(items)` | Pushes each element of `items` on top of the stack, one at a time. 25 | `$` | Returns the number of items in the stack. 26 | `?` | Returns `1` if the stack is not empty, otherwise returns `0`.[^2] 27 | `!` | Returns some information about the stack as a string;
its capacity, number of items, and the value of the top item. 28 | 29 |
30 | 31 | 32 | ## Queue 33 | 34 | A queue is a collection of items that the user may "put" ("enqueue") an item at 35 | the back of, or "get" ("dequeue") an item from the front of, following a first 36 | in first out order (FIFO). 37 | 38 |
39 | 40 | Method                    | Use 41 | --- | --- 42 | `=>([size])` | Initializes an empty Queue object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the queue unbounded capacity. 43 | `first()` | Returns the value of the item at the front of the queue, without removing it.
If the queue is empty, this will instead throw an error. 44 | `get()` | Gets/removes an item from the front of the queue, and returns it.
If the queue is empty, this will instead throw an error. 45 | `is_empty()` | Returns `1` if the number of items in the queue is equal to 0, otherwise returns `0`. 46 | `is_full()` | Returns `1` if the number of items in the queue is equal to the specified capacity,
otherwise returns `0`.[^1] 47 | `last()` | Returns the value of the item at the back of the queue, without removing it.
If the queue is empty, this will instead throw an error. 48 | `put(item)` | Puts `item` at the back of the queue. If the queue is full,
i.e. its size is equal to the specified capacity, this will instead throw an error. 49 | `put_all(items)` | Puts each element of `items` at the back of the queue, one at a time. 50 | `->?(item)` | Returns `1` if `item` is present in the queue, `0` otherwise. 51 | `$` | Returns the number of items in the queue. 52 | `?` | Returns `1` if the queue is not empty, otherwise returns `0`.[^2] 53 | `!` | Returns some information about the queue as a string;
its capacity, number of items, and the values of its items.
Note that if there are more than 5 items in the queue, the string will be truncated. 54 | 55 |
56 | 57 | 58 | ## Deque 59 | 60 | A deque is a data structure similar to a queue, but where insertion and removal 61 | of elements can be performed from both the front and the back. 62 | 63 |
64 | 65 | Method                               | Use 66 | --- | --- 67 | `=>([size])` | Initializes an empty Deque object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the deque unbounded capacity. 68 | `back()` | Returns the value of the item at the back of the deque, without removing it.
If the deque is empty, this will instead throw an error. 69 | `front()` | Returns the value of the item at the front of the deque, without removing it.
If the deque is empty, this will instead throw an error. 70 | `get()` | Gets/removes an item from the back of the deque, and returns it.
If the deque is empty, this will instead throw an error. 71 | `get_front()` | Gets/removes an item from the front of the deque, and returns it.
If the deque is empty, this will instead throw an error. 72 | `is_empty()` | Returns `1` if the number of items in the deque is equal to 0, otherwise returns `0`. 73 | `is_full()` | Returns `1` if the number of items in the deque is equal to the specified capacity,
otherwise returns `0`.[^1] 74 | `put(item)` | Puts `item` at the back of the deque. If the deque is full,
i.e. its size is equal to the specified capacity, this will instead throw an error. 75 | `put_all(items)` | Puts each element of `items` at the back of the deque, one at a time. 76 | `put_front(item)` | Puts `item` at the front of the deque. If the deque is full,
i.e. its size is equal to the specified capacity, this will instead throw an error. 77 | `put_front_all(items)` | Puts each element of `items` at the front of the deque, one at a time. 78 | `$` | Returns the number of items in the deque. 79 | `?` | Returns `1` if the deque is not empty, otherwise returns `0`.[^2] 80 | `!` | Returns some information about the deque as a string;
its capacity, number of items, and the values of its items.
Note that if there are more than 5 items in the deque, the string will be truncated. 81 | 82 |
83 | 84 | 85 | ## Set 86 | 87 | A set is an unordered collection of items, with no duplicates. 88 | 89 |
90 | 91 | Method                                     | Use 92 | --- | --- 93 | `=>([items][, capacity])` | Initializes a `Set` object, with its contents being `items` with any
duplicate elements removed, and its capacity being `capacity`.
If `items` is unspecified it will default to an empty array.
If `capacity` is unspecified it will default to `-1`,
giving the set unbounded capacity. 94 | `items` | The contents of the set as an array. 95 | `add(value)` | Adds `value` to the set, provided it doesn't already exist in the set,
and returns a status code (`0` or `1`) based on whether it was added.
If `value` isn't already in the set, and the set is full,
i.e. its size is equal to the specified capacity, this will instead throw an error. 96 | `clear()` | Removes every element from the set. 97 | `value ->?` | Returns `1` if `value` is contained in the set, otherwise returns `0`. 98 | `is_empty()` | Returns `1` if the number of items in the set is equal to 0,
otherwise returns `0`. 99 | `is_full()` | Returns `1` if the number of items in the set
is equal to the specified capacity, otherwise returns `0`.[^1] 100 | `remove(value)` | Removes `value` from the set, provided it exists in the set. 101 | `- other` | Returns the difference of the current set and `other`. 102 | `& other` | Returns the intersection of the current set and `other`. 103 | `:: other` | Returns `1` if the current set and `other` have the same elements, `0` otherwise. 104 | `::: other` | Returns `1` if the current set and `other`
don't have the same elements, `0` otherwise. 105 | `<: other` | Returns `1` if the current set is a subset of `other`. 106 | `< other` | Returns `1` if the current set is a strict subset of `other`. 107 | `>: other` | Returns `1` if the current set is a superset of `other`. 108 | `> other` | Returns `1` if the current set is a strict superset of `other`. 109 | `| other` | Returns the union of the current set and `other`. 110 | `$` | Returns the number of items in the set. 111 | `?` | Returns `1` if the deque is not empty, otherwise returns `0`.[^2] 112 | `!` | Returns some information about the set as a string;
its capacity, number of items, and the values of its items. 113 | 114 |
115 | 116 | 117 | ## ArithmeticArray 118 | 119 | An arithmetic array is an array which can be used with different binary 120 | operators. 121 | 122 | ```sm 123 | <=collections.ArithmeticArray; 124 | 125 | aa: ArithmeticArray([/\, //, /\/]); 126 | 127 | aa!; == [2, 3, 5] 128 | aa + /\!; == [4, 5, 7] 129 | aa ++ ///!; == [14, 21, 35] 130 | 131 | is_odd: aa --- /\; 132 | is_odd!; == [0, 1, 1] 133 | 134 | aa: ArithmeticArray(["oh", "hey", "hello"]); 135 | aa + "!"!; == ["oh!", "hey!", "hello!"] 136 | ``` 137 | 138 | Binary operators supported by ArithmeticArray: 139 | - arithmetic: `+`, `++`, `+++`, `-`, `--`, `---` 140 | - bitwise: `&`, `|`, `^` 141 | - comparison: `::`, `>:`, `>`, `<:`, `<`, `:::` 142 | 143 | ArithmeticArray allows item assignment and inherits behavior for `$`, `to_bit`, 144 | and `to_string` from the `Array` class. 145 | 146 | You can also use your own custom operators in the form of functions by using the 147 | `apply(op, other)` method: 148 | 149 | ```sm 150 | <=collections.ArithmeticArray; 151 | 152 | aa: ArithmeticArray([///, /\//, //\/]); 153 | aa!; 154 | aa.apply(<-math.shl, /\)!; 155 | 156 | 157 | remove_null nullable default * { 158 | * default ? nullable :: _ ,, nullable; 159 | } 160 | 161 | aa: ArithmeticArray([_, /\, //, _, /\/, _, ///]); 162 | aa!; == [null, 2, 3, null, 5, null, 7] 163 | aa.apply(remove_null, \)!; == [0, 2, 3, 0, 5, 0, 7] 164 | ``` 165 | 166 | 167 | [^1]: Note that this will always return `0` if the specified capacity is 168 | negative, or if the user does not provide a capacity. 169 | 170 | [^2]: `?` is functionally the opposite of `is_empty()`. 171 | -------------------------------------------------------------------------------- /docs/stddatetime.md: -------------------------------------------------------------------------------- 1 | # `datetime` module 2 | 3 | The `datetime` module consists of several functions that assist with date and 4 | time related tasks. 5 | Additionally, it includes two classes (`DateTime` and `DTDiff`). 6 | 7 |
8 | 9 | Variable | Contents 10 | --- | --- 11 | MONTHS | `["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]` 12 | WEEKDAYS | `["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]` 13 | 14 | Function                                             | Use 15 | --- | --- 16 | `sleep_seconds(seconds)` | Pauses execution for `seconds` seconds. 17 | `sleep_minutes(minutes)` | Pauses execution for `minutes` minutes. 18 | `is_leap_year(year)` | Returns `1` if the year is a leap year, `0` otherwise. 19 | `days_in_month(year, month)` | Returns the amount of days in a given month keeping in track leapyears. 20 | `month_name(n)` | Gives the name of a numbered month where `1` is `January`. 21 | `weekday_name(n)` | Gives the day of a week from a number
where `1` is a `Monday` and `7` is `Sunday`. 22 | `timestamp_utc([ts])` | Yields the `UTC` timestamp using the provided timestamp or current time. 23 | 24 |
25 | 26 | ## DateTime 27 | 28 |
29 | 30 | Method                      | Use 31 | --- | --- 32 | `subtract(other)` | Subtract two DateTime objects, returns a DTDiff object. 33 | `to_timestamp()` | Returns the date & time as a Unix timestamp in milliseconds. 34 | `!` | Return the date and time in the format `Y-M-D h:m:s.z`. 35 | 36 |
37 | -------------------------------------------------------------------------------- /docs/stdio.md: -------------------------------------------------------------------------------- 1 | # `io` module 2 | 3 | The `io` module contains a few utilities for working with I/O. 4 | 5 | ### `io.Bytes` 6 | Allows for easier working with files in binary mode: 7 | ```sm 8 | b: <-io.Bytes("ball"); 9 | b!; == 62 61 6c 6c 10 | b+: "!"; == supports Strings, integers, Arrays of integers, and other Bytes 11 | b.export_string()!; == ball! 12 | b.export()!; == [98, 97, 108, 108, 33] 13 | ``` 14 | 15 | ### `io.inputcast([prompt])` 16 | Works just like `???`, but tries converting the input to a specific type. 17 | 18 | Example (showing all available conversions): 19 | ```sm 20 | <=io.inputcast; 21 | 22 | .. { 23 | value: inputcast(">> "); 24 | "Type:", value?!!; 25 | "Value:", value!; 26 | } 27 | ``` 28 | ``` 29 | >> hello 30 | Type: String 31 | Value: hello 32 | >> hello there 33 | Type: Array 34 | Value: ["hello", "there"] 35 | >> 1 36 | Type: Number 37 | Value: 1 38 | >> 1 2 39 | Type: Array 40 | Value: [1, 2] 41 | >> 1, 2 42 | Type: Array 43 | Value: [1, 2] 44 | >> 45 | Type: Null 46 | Value: null 47 | >> a=1, b=2 48 | Type: Table 49 | Value: {{"a" -> 1, "b" -> 2}} 50 | >> 3..7 51 | Type: Slice 52 | Value: <<3..7>> 53 | ``` 54 | 55 | ### `io.read_until([target])` 56 | Keeps reading lines until the `target` (`""` by default) is entered. 57 | The `target` is included in the output string. 58 | 59 | Example: 60 | ```sm 61 | "Enter your JSON:"!; 62 | program: <-io.read_until("}"); 63 | name: "Enter file name: "???; 64 | program ~> name; 65 | "Your program was saved to " + name!; 66 | ``` 67 | ``` 68 | Enter your JSON: 69 | { 70 | "name": "John Doe", 71 | "age": 30, 72 | "email": "johndoe@example.com", 73 | "interests": [ 74 | "reading", 75 | "hiking", 76 | "cooking" 77 | ] 78 | } 79 | Enter file name: ball.json 80 | Your program was saved to ball.json 81 | ``` -------------------------------------------------------------------------------- /docs/stditer.md: -------------------------------------------------------------------------------- 1 | # `iter` module 2 | 3 | The `iter` module contains several functions that interact with iterable 4 | objects, like strings or arrays. 5 | 6 |
7 | 8 | Function                                                  | Use 9 | --- | --- 10 | `accumulate(array, function)` | Yields accumulated applications of `function`[^2]
on consecutive elements of `array`.
If for example `function` returned the sum of both of its arguments,
then `accumulate([/, /\, //, /\\, /\/], function)`
would yield `1`, `3`, `6`, `10`, and `15`. 11 | `all(array)` | Returns `1` if all elements of `array` are truthy,
`0` otherwise. Returns `1` for empty arrays. 12 | `any(array)` | Returns `1` if any of the elements of `array` is truthy,
`0` otherwise. Returns `0` for empty arrays. 13 | `chunks(array, size)` | Iterates over `array` in chunks of size `size`.
When `array`'s length is not evenly divided by `size`,
the last slice of `array` will be the remainder. 14 | `cycle(iter)` | Copies an iterable by consuming it,
and yields its elements in an infinite cycle. 15 | `count(array, target)` | Returns the number of times `target` appears in `array`. 16 | `drop_while(array, function)` | Evaluates `function`[^1] on each item of `array`,
and yields elements of `array` starting from the first item
(from the left) for which `function` returns a falsy value. 17 | `filter(function, array)` | Evaluates `function`[^1] on each item of `array`,
and yields those items that cause `function` to return a truthy value.
`function`s taking *x* (≥2) arguments require
each element of `array` to have *x* elements. 18 | `filter_false(function, array)` | Evaluates `function`[^1] on each item of `array`,
and yields those items that cause `function` to return a falsy value. 19 | `find(array, target)` | Finds the first instance of `target` in `array`, and returns its index.
`array` may be of type Array or String.
If `target` does not appear in `array`, `-1` is returned instead. 20 | `find_all(array, target)` | Finds all instances of `target` in `array`, and yields their indices. 21 | `flatten(array[, depth])` | Flattens `array` `depth` times.
By default, flattens recursively as deep as possible. 22 | `map(function, array)` | Applies `function`[^1] to each item of `array`, and yields those new values.
`function`s taking *x* (≥2) arguments require
each element of `array` to have *x* elements. 23 | `pairwise(array)` | Yields successive overlapping pairs taken from `array`. 24 | `reduce(function, array)` | Applies `function`[^2] cumulatively to consecutive items of `array`,
reducing it to a single value, then returns this value.
Equivalent to `[i ... i ->? accumulate(array, function)]<<-/>>`. 25 | `reverse(array)` | Yields the items of `array` in reverse order. 26 | `sorted(array[, key])` | Returns a sorted copy of `array`.
The optional parameter `key` specifies a function[^1] that is used
to extract a comparison key from each element in `array`.
Elements are compared directly by default. 27 | `take_while(array, function)` | Evaluates `function`[^1] on each item of `array`,
and yields elements of `array` that is cut off at the first item
(from the left) for which `function` returns a falsy value. 28 | `zip_longest(fill, arrays...)` | Iterates over several arrays, producing a set of arrays
containing an item from each original array.
If the arrays are of uneven length,
missing values are filled using the `fill` argument. 29 | 30 |
31 | 32 | [^1]: Note that `function` must take only one argument (excluding optional 33 | parameters). 34 | 35 | [^2]: Note that `function` must take exactly two arguments (excluding optional 36 | parameters). 37 | -------------------------------------------------------------------------------- /docs/stdmath.md: -------------------------------------------------------------------------------- 1 | # `math` module 2 | 3 | The `math` module provides access to a set of commonly used mathematical 4 | functions and constants. 5 | 6 | Constants: 7 | 8 | - `math.E` (e: 2.718281828459045...) 9 | - `math.PHI` (φ: 1.618033988749894...) 10 | - `math.PI` (π: 3.141592653589793...) 11 | - `math.TAU` (τ: 6.283185307179586...) 12 | 13 |
14 | 15 | Function                             | Use 16 | --- | --- 17 | `abs(n)` | Returns the absolute value of `n`. 18 | `ceil(x)` | Returns the least integer ≥ `x`. 19 | `factorial(n)` | Returns `n` factorial. 20 | `floor(x)` | Returns the greatest integer ≤ `x`. 21 | `gcd(a, b)` | Returns the greatest common divisor of `a` and `b`.
If either argument is zero, the absolute value of the other argument will be returned. 22 | `is_int(x)` | Returns `1` if `x` is an integer, `0` otherwise. Equivalent to `x :: x$`. 23 | `is_prime(n)` | Returns `1` if `n` is prime, `0` otherwise. 24 | `lcm(a, b)` | Returns the least common multiple of `a` and `b`.
If any of the arguments is zero,
then the returned value is `0`. 25 | `max(array[, values...])`  | Returns the largest value in `array` if only one argument is passed,
otherwise returns the largest value in `[array] + values`. 26 | `min(array[, values...])`  | Returns the smallest value in `array` if only one argument is passed,
otherwise returns the smallest value in `[array] + values`. 27 | `product(array)` | Multiplies the items of `array` from left to right and returns the total.
The `array`'s items must be numbers. 28 | `round(x[, ndigits])` | Returns `x` rounded to `ndigits` precision after the decimal point.
Works exactly like Python's [`round()`](https://docs.python.org/3/library/functions.html#round). 29 | `shl(a, b)` | Returns `a` shifted to the left by `b` bits. 30 | `shr(a, b)` | Returns `a` shifted to the right by `b` bits. 31 | `sqrt(x)` | Returns the square root of the nonnegative number `n`. 32 | `sum(array[, start])` | Sums `start` and the items of `array` from left to right and returns the total.
`start` defaults to `0`. 33 | `to_bin(n)` | Returns the binary representation of `n` as a string. 34 | `to_oct(n)` | Returns the octal representation of `n` as a string. 35 | `to_hex(n)` | Returns the hexadecimal representation of `n` as a string. 36 | 37 |
38 | -------------------------------------------------------------------------------- /docs/stdoperator.md: -------------------------------------------------------------------------------- 1 | # `operator` module 2 | 3 | This module contains a set of functions corresponding to the native operators 4 | of Samarium. For instance, `operator.mul(a, b)` is equivalent to `a ++ b`. 5 | Each of the function names can be used for defining special methods in classes. 6 | 7 |
8 | 9 | Function               | Operator                10 | --- | :---: 11 | `add(x, y)` | `x + y` 12 | `and(x, y)` | `x & y` 13 | `cast(x)` | `x%` 14 | `div(x, y)` | `x -- y` 15 | `eq(x, y)` | `x :: y` 16 | `ge(x, y)` | `x >: y` 17 | `gt(x, y)` | `x > y` 18 | `has(x, y)` | `y ->? x` 19 | `hash(x)` | `x##` 20 | `le(x, y)` | `x <: y` 21 | `lt(x, y)` | `x < y` 22 | `mod(x, y)` | `x --- y` 23 | `mul(x, y)` | `x ++ y` 24 | `not(x)` | `~x` 25 | `ne(x, y)` | `x ::: y` 26 | `or(x, y)` | `x | y` 27 | `pow(x, y)` | `x +++ y` 28 | `random(x)` | `x??` 29 | `special(x)` | `x$` 30 | `sub(x, y)` | `x - y` 31 | `to_bit(x)` | `/ ? x ,, \` 32 | `to_string(x)` | `""?!(x)` 33 | `xor(x, y)` | `x ^ y` 34 | 35 |
36 | -------------------------------------------------------------------------------- /docs/stdrandom.md: -------------------------------------------------------------------------------- 1 | # `random` module 2 | 3 | The `random` module implements functions that make use of the 4 | [random method `??`](builtins.md#RANDOM). 5 | 6 | Function                      | Use 7 | --- | --- 8 | `choices(iter, k)` | Randomly selects an item from `iter` `k` times
and returns the resulting choices as an array. 9 | `randint(a, b)` | Returns a random integer in range [a, b]. 10 | `sample(array, k)` | Randomly selects `k` unique items from `array`,
and returns the resulting choices as an array.
Also accepts slices. 11 | `shuffle(array)` | Randomly shuffles `array` and returns the result.
`array` must be of type Array. 12 | -------------------------------------------------------------------------------- /docs/stdstring.md: -------------------------------------------------------------------------------- 1 | # `string` module 2 | 3 | The `string` module contains several useful functions for string manipulation, 4 | as well as some variables containing groups of similar characters. 5 | 6 |
7 | 8 | Variable           | Contents                                                                                  9 | --- | --- 10 | `DIGITS` | `"0123456789"` 11 | `HEXDIGITS` | `"0123456789abcdef"` 12 | `LETTERS` | `UPPERCASE + LOWERCASE` 13 | `LOWERCASE` | `"abcdefghijklmnopqrstuvwxyz"` 14 | `OCTDIGITS` | `"01234567"` 15 | `PRINTABLE` | `LETTERS + DIGITS + PUNCTUATION + WHITESPACE` 16 | `PUNCTUATION` | ``"!\"#$%&'()*+,-./:;<=>?@[]^_`{\|}~"`` 17 | `UPPERCASE` | `"ABCDEFGHIJKLMNOPQRSTUVWXYZ"` 18 | `WHITESPACE` | `" \t\n\r\f\v"` 19 | 20 | Function                                                                | Use 21 | --- | --- 22 | `capitalize(string)` | Returns a copy of `string` with the first character
set to uppercase (assuming it's a cased character[^2])
and all subsequent character set to lowercase. 23 | `center(string, length[, char])`[^1] | Returns `string` centered in a new string of length `length`,
padded using the specified `char`.
If `char` is not specified, it defaults to `" "` (space).
`string` is returned unchanged if `length` is
less than or equal to the length of `string`. 24 | `ends_with(string, suffix)` | Returns `1` if `string` ends with the substring `suffix`,
otherwise returns `0`. 25 | `is_alphabetic(string)` | Returns `1` if every character in `string` is
an alphabetic character,
i.e. is contained in the string `letters`, otherwise returns `0`. 26 | `is_alphanumeric(string)` | Returns `1` if every character in `string` is
an alphanumeric character,
i.e. is contained in the strings `letters` or `numbers`,
otherwise returns `0`. 27 | `is_capitalized(string)` | Returns `1` if `string` is capitalized,
i.e. matches the output of `capitalize(string)` exactly. 28 | `is_decimal(string)` | Returns `1` if every character in `string` is a decimal digit,
i.e. is contained in the string `DIGITS`, otherwise returns `0`. 29 | `is_hexadecimal(string)` | Returns `1` if every character in `string` is a hexadecimal digit,
i.e. is contained in the string `HEXDIGITS`, otherwise returns `0`. 30 | `is_in_group(string, group)` | Returns `1` if every character in `string` is
in the specified `group`
of type Array or String, otherwise returns `0`. 31 | `is_lower(string)` | Returns `1` if every cased character[^2] in `string` is lowercase,
otherwise returns `0`. 32 | `is_octal(string)` | Returns `1` if every character in `string` is an octal digit,
i.e. is contained in the string `OCTDIGITS`, otherwise returns `0`. 33 | `is_title(string)` | Returns `1` if `string` is in title case,
i.e. matches the output of `title(string)` exactly. 34 | `is_upper(string)` | Returns `1` if every cased character[^2] in `string` is uppercase,
otherwise returns `0`. 35 | `is_wrapped(string, chars)` | Returns `1` if `string` both starts and ends with
the substring `chars`, otherwise returns `0`. 36 | `join(iterable[, delimiter])` | Returns a string with each consecutive member
of `iterable` converted to a string
and joined with `delimiter` between them.
If `delimiter` is not specified, it defaults to `" "`. 37 | `leftpad(string, length[, char])` | Returns a copy of `string` padded on the left so that
it's `length` characters long, using `char` for padding.
If `char` is not specified, it defaults to `" "`.
If `length` is shorter than `string`'s length,
a copy of `string` is returned. 38 | `ordinal(n)` | Returns an ordinal numeral of a number,
e.g. `ordinal(/)` returns `"1st"`. 39 | `replace(string, replacement[, count])` | Returns a copy of `string`, with all instances of each key
in the `replacement` table replaced with its corresponding value.
If `count` is specified, only the first `count` instances of each key
will be replaced, starting from the left. 40 | `rightpad(string, length[, char])` | Returns a copy of `string` padded on the right so that
it's `length` characters long, using `char` for padding.
If `char` is not specified, it defaults to `" "`.
If `length` is shorter than `string`'s length,
a copy of `string` is returned. 41 | `split(string[, separator])` | Returns an array of the words in `string`,
separated by `separator`.
If `separator` is not specified, it defaults to `" "`.
If `separator` is an Array, all of its elements will be used as separators. 42 | `split_lines(string)` | Returns an array of the lines in the string,
breaking at line boundaries.
Line breaks are not included in the resulting array. 43 | `starts_with(string, prefix)` | Returns `1` if `string` starts with the substring `prefix`,
otherwise returns `0`. 44 | `strip(string, chars)` | Returns a copy of `string` with `chars` removed from
both the beginning and the end,
as in `strip_left` and `strip_right`. 45 | `strip_left(string, prefix)` | Returns a copy of `string` with `prefix` removed
from the beginning,
multiple times if `string` still begins with `prefix`.
If `string` doesn't begin with `prefix`,
a copy of the original `string` is returned. 46 | `strip_right(string, suffix)` | Returns a copy of `string` with `suffix` removed from the end,
multiple times if `string` still ends with `suffix`.
If `string` doesn't end with `suffix`,
a copy of the original `string` is returned. 47 | `swapcase(string)` | Returns a copy of `string` with every cased character[^2]
set to the opposite of its original case. 48 | `title(string)` | Returns a copy of `string` with the first character of each word
(separated by spaces) set to uppercase (assuming they're
cased characters[^2]), and all subsequent characters
of each word set to lowercase. 49 | `to_lower(string)` | Returns a copy of `string` with every
cased character[^2] set to lowercase. 50 | `to_upper(string)` | Returns a copy of `string` with every
cased character[^2] set to uppercase. 51 | `wrap(string, wrapper)` | Returns a copy of `string` with
`wrapper` added to the start and end. 52 | 53 |
54 | 55 | [^1]: An argument in `[square brackets]` means that it has a default value, 56 | and so it isn't necessary to give it a value. 57 | 58 | [^2]: Cased characters are alphabetic characters in either uppercase 59 | or lowercase; `LETTERS` is a string of all cased characters. 60 | -------------------------------------------------------------------------------- /docs/stdtypes.md: -------------------------------------------------------------------------------- 1 | # `types` module 2 | 3 | The `types` module implements native data type aliases and data types not 4 | built-in to the language itself. 5 | 6 | 7 | ## Additional types 8 | 9 | 10 | ### Boolean 11 | 12 | The Boolean type can have value either `true` or `false`. 13 | 14 | Booleans can be initialized with either a string with the value `"true"` or 15 | `"false"`, or a truthy/falsy value. Booleans support several operations: all 16 | comparison operations, all arithmetic operations, all bitwise operations, and 17 | can be used with logical operators, and converted to a string (specifically 18 | either `"true"` or `"false"`). 19 | 20 | 21 | ### Frozen 22 | 23 | `Frozen` is a wrapper type designed to allow mutable types like Arrays and 24 | Tables to be used as keys in a Table. `Frozen` is immutable, meaning that once a 25 | value has been wrapped in a Frozen, it cannot be changed. 26 | 27 | ```sm 28 | test_key key * { 29 | ?? { {{key -> <-math.sum(key)}}!; } 30 | !! { "invalid key:", key!; } 31 | } 32 | 33 | test_key([//, /\\]); == invalid key: [3, 4] 34 | test_key(<-types.Frozen([//, /\\])); == {{Frozen([3, 4]) -> 7}} 35 | ``` 36 | 37 | 38 | 39 | ### UUID4 40 | 41 | Generates a random UUID, version 4. 42 | Returns a UUID object with attributes `hex` and `dec`: 43 | 44 | ```sm 45 | <=types.UUID4; 46 | 47 | uuid: UUID4(); 48 | uuid!; 49 | == 8b46f521-b821-4010-ae8f-9ae9522d9889 50 | 51 | uuid.hex!; 52 | == "8b46f521b8214010ae8f9ae9522d9889" 53 | 54 | uuid.dec!; 55 | == 185131124056068440795959350641466120329 56 | ``` 57 | 58 | 59 | ## Built-in type aliases 60 | 61 | 62 | ### Array 63 | 64 | The `Array` type alias is defined to be equal to `[]?!`. 65 | 66 | Calling `Array()` with no arguments will return an empty array. 67 | 68 | A copy of an array `a` can be made by using `Array(a)`. 69 | 70 | ```sm 71 | a: [/, /\, //]; 72 | b: a; 73 | c: Array(a); 74 | d: a<<>>; 75 | 76 | a<>: /\\; 77 | a, b, c, d!; 78 | ``` 79 | 80 | Arrays can also be constructed from strings and tables: 81 | 82 | > `Array("ball")` is equivalent to `["b", "a", "l", "l"]` 83 | > `Array({{// -> /\\, "X" -> "D" }})` is equivalent to `[[//, /\\], ["X", "D"]]` 84 | 85 | 86 | ### Number 87 | 88 | The `Number` type alias is defined to be equal to `\?!`. 89 | 90 | Caling `Number()` with no arguments will return the number `0`. 91 | 92 | Numbers can be constructed from strings, including 93 | binary, octal, and hexadecimal representations: 94 | 95 | > `Number("1000")` will return `1000` 96 | > `Number("b:1000")` will return `8` 97 | > `Number("o:1000")` will return `512` 98 | > `Number("x:1000")` will return `4096` 99 | 100 | Floats can also be supplied: 101 | > `Number("3.14")` will return `3.14` 102 | > `Number("x:3.23d70a3d70a3e")` will return `3.14` 103 | > `Number("o:23.6560507534121727")` will return `19.84` 104 | 105 | Scientific notation is also supported, with `significand[e]exponent` for bases 106 | 2, 8 and 10, and `significand[p]exponent` for base 16: 107 | > `Number("b:0.11e11")` will return `6` 108 | > `Number("o:5e7")` will return `10485760` 109 | > `Number("1e-3")` will return `0.001` 110 | > `Number("x:0.2p7")` will return `16` 111 | 112 | 113 | ### Null 114 | 115 | The `Null` type alias is defined to be equal to `(||)?!`. It can be used for 116 | explicit null values instead of relying on implicit null triggers. 117 | 118 | 119 | ### Slice 120 | 121 | The `Slice` type alias is defined to be equal to `<<>>?!`. 122 | 123 | Different slices can be constructed by using integers and nulls: 124 | 125 | - `Slice(/, /\, //)` is equivalent to `<>` 126 | - `Slice(, , -/)` is equivalent to `<<....-/>>` 127 | - `Slice(/, , ,)` is equivalent to `<>` 128 | - `Slice(, /\/, ,)` is equivalent to `<<../\/>>` 129 | 130 | 131 | ### String 132 | 133 | The `String` type alias is defined to be equal to `""?!`. 134 | 135 | Calling `String()` with no arguments will return an empty string. 136 | 137 | Any data type can be converted to a string. 138 | 139 | A copy of a string `s` can be made by using `String(s)`. 140 | 141 | 142 | ### Table 143 | 144 | The `Table` type alias is defined to be equal to `{{}}?!`. 145 | 146 | Calling `Table()` with no arguments will return an empty table. 147 | 148 | Tables can be constructed from arrays containing 2-element iterables:
149 | `Table([[//, /\\/], "XD"])` is equivalent to `{{// -> /\\/, "X" -> "D"}}` 150 | 151 | ### Zip 152 | 153 | The `Zip` type alias is defined to be equal to `("" >< "")?!`. 154 | 155 | Calling `Zip()` with no arguments will return an empty iterator. 156 | Passing in only 1 argument is equivalent to `... i ->? iter { ** [i]; }`. 157 | Calling `Zip()` with 2 or more arguments is equivalent to using the `><` 158 | operator on them. -------------------------------------------------------------------------------- /docs/strings.md: -------------------------------------------------------------------------------- 1 | # Strings 2 | 3 | Strings are defined using double quotation marks: 4 | 5 | ```sm 6 | str: "Hello!"; 7 | ``` 8 | 9 | Multiline strings do not require any additional syntax: 10 | 11 | ```sm 12 | "This 13 | is a 14 | multiline 15 | string" 16 | ``` 17 | 18 | 19 | ## Basic Operations 20 | 21 | Strings can be manipulated using some arithmetic operators: 22 | 23 | > `"hello" + "world"` is the same as `"helloworld"` 24 | 25 | > `"hello" ++ //` (or `// ++ "hello"`) is the same as `"hellohellohello"` 26 | > ``"ball" ++ `/`` is the same as `"ba"` 27 | > `"woah!" ++ <-math.PI` is the same as `"woah!woah!woah!w"` 28 | > `"hello"++` is the same as `"hello" ++ /\` 29 | 30 | > `-"hello"` is the same as `"olleh"` 31 | 32 | > `"hello" - "l"` is the same as `"helo"` (the 2nd operand removes the first 33 | > instance of itself from the 1st operand) 34 | 35 | > `"hello" -- "l"` is the same as `"heo"` (the 2nd operand removes all 36 | > occurences of itself from the 1st operand) 37 | 38 | !!! warning 39 | Multiplying strings by negative numbers is undefined, as it is unclear 40 | whether `s ++ -x` means `-(s ++ x)` or `(-s) ++ x`. These are equivalent for 41 | integers, but they produce different results for other numbers: 42 | 43 | - ``-("hello" ++ /`/)`` is `"eholleh"` 44 | - ``(-"hello") ++ /`/`` is `"ollehol"` 45 | 46 | 47 | ## Formatting 48 | 49 | Strings can be formatted using the `---` operator. 50 | The 2nd operand can be a String, Array, or a Table. 51 | ```sm 52 | "Hi $0!" --- "Bob"!; 53 | == Hi Bob! 54 | 55 | "$0$1$0" --- ["abra", "cad"]!; 56 | abracadabra 57 | 58 | s: "Coordinates: $lat, $long"; 59 | coords: {{ 60 | "lat" -> "56.37N", 61 | "long" -> "-8.34W" 62 | }}; 63 | s --- coords!; 64 | == Coordinates: 56.37N, -8.34W 65 | ``` 66 | 67 | 68 | ## Casting 69 | 70 | Strings can be cast to integers (for single characters) or Arrays of integers 71 | (for longer strings), representing the Unicode code point of each character: 72 | 73 | > `"a"%` returns `97` 74 | > `"hi!"%` returns `[104, 105, 33]` 75 | 76 | 77 | ## Shifting 78 | 79 | Strings can have their characters shifted by adding/subtracting numbers. 80 | For example, `"hi" + /` will result in the string `"ij"`, where each character 81 | in the string has been shifted one position ahead. Similarly, `"hi" - /` will 82 | result in the string `"gh"`, where each character in the string has been shifted 83 | one position backwards. 84 | 85 | > `"hi"+` is the same as `"hi" + /` 86 | > `"hi"-` is the same as `"hi" - /` 87 | -------------------------------------------------------------------------------- /docs/tables.md: -------------------------------------------------------------------------------- 1 | # Tables 2 | 3 | Tables map hashable values to arbitrary objects. 4 | They are defined using double curly brackets, with `->` mapping each key to 5 | each value: 6 | 7 | ```sm 8 | tab: {{"key" -> "value", / -> [/\, "a"]}}; 9 | ``` 10 | 11 | A table may be indexed by its keys, which will return their corresponding 12 | values, for example, from the previous table: 13 | 14 | > `tab<<"key">>` returns `"value"` 15 | > tab<</>> returns `[2, "a"]` 16 | 17 | After its initialization, the items of a table can be set using this indexing 18 | syntax. If the key doesn't already exist in the table, it will be created. 19 | 20 | > `tab<<"key">>: "newvalue"` will overwrite the previous value of `tab<<"key">>`. 21 | 22 | > `tab<<"newkey">>: //\` will create a new item in the table, with key `"newkey"` 23 | and value `6`. 24 | 25 | Tables can be merged together with the addition operator `+`. The values of the 26 | table to the right of the operator take priority when both tables share keys. 27 | 28 | Items can be removed from a table by key using the subtraction operator `-`: 29 | 30 | > `{{"a" -> /, "b" -> /\, // -> /\//}} - "b"` gives `{{"a" -> /, // -> /\//}}` 31 | 32 | Tables can be inverted using the unary `~` operator: 33 | > `~{{"a" -> /, "b" -> /\}}` gives `{{/ -> "a", /\ -> "b"}}` 34 | -------------------------------------------------------------------------------- /docs/tools.md: -------------------------------------------------------------------------------- 1 | # Shebang 2 | 3 | You can easily use your Samarium scripts on Unix by putting an appropriate 4 | shebang line at the top of your program, for instance 5 | ```sm 6 | #!/usr/bin/env samarium 7 | 8 | "Hi!"!; 9 | ``` 10 | and making it executable, e.g. with 11 | ```bash 12 | $ chmod +x script 13 | ``` 14 | 15 | 16 | # Samarium REPL 17 | 18 | If you run the `samarium` command without any arguments, 19 | you'll launch the REPL, an interactive shell that will read 20 | and evaluate any Samarium code you enter. 21 | ```txt 22 | $ samarium 23 | Samarium 0.6.2 24 | --> 25 | ``` 26 | Interacting with the REPL is a nice way to experiment with Samarium: 27 | ```txt 28 | --> / + /\ 29 | 3 30 | --> 1: "ball"##! 31 | 1083481267058749873 32 | --> 1?? 33 | 443527852557841359 34 | --> 1?? 35 | 894622914084910886 36 | ``` 37 | The REPL also supports compound statements: 38 | ```txt 39 | --> x: /\/\?? 40 | --> ? x --- /\ :: \ { 41 | > x -- /\!; 42 | > } ,, { 43 | > 3 ++ x + /!; 44 | > } 45 | 4 46 | --> x 47 | 8 48 | ``` 49 | 50 | ## Commands 51 | 52 | Samarium 0.6.0 introduced commands to improve your REPL experience. 53 | Commands are prefixed with a colon, use `:?` to see the list of all commands. 54 | 55 | 56 | ## `clear` 57 | Clears the screen. 58 | 59 | 60 | ## `color [color|save]` 61 | Changes the prompt color. The available colors are [Dahlia codes] from `0` to 62 | `e`, as well as their English names. Providing no argument will reset the 63 | color. `:color save` will save the current color to the REPL config, making the 64 | change permanent. 65 | 66 | The English names can be checked by using the `:? color` command: 67 | ``` 68 | --> :? color 69 | color [color] 70 | providing no color will reset it to the default one 71 | 72 | use :color save to save the current color to your config 73 | 74 | 0|black 75 | 1|blue 76 | 2|green 77 | 3|cyan 78 | 4|red 79 | 5|purple 80 | 6|orange 81 | 7|light gray 82 | 8|gray 83 | 9|light blue 84 | a|lime 85 | b|aquamarine 86 | c|light red 87 | d|pink 88 | e|yellow 89 | ``` 90 | 91 | 92 | ## `debug` 93 | Toggles debug mode which shows the intermediary Python code that the Samarium 94 | input is transpiled to before executing it (equivalent to using the 95 | `samarium-debug` command). 96 | ``` 97 | --> / 98 | 1 99 | --> :debug 100 | --> / 101 | Num(1) 102 | 1 103 | ``` 104 | 105 | 106 | ## `exit` 107 | > Aliases: `q`, `quit` 108 | 109 | Exits the REPL and saves the session if [autosave](#autosave-truefalse) is 110 | enabled. Appending a `!` or just using it as a command will "force quit" the 111 | REPL without saving the session. 112 | ``` 113 | $ ls -1 ~/.cache/samarium | wc -l 114 | 7 115 | $ samarium 116 | Samarium 0.6.2 117 | --> :session autosave 118 | Autosave is enabled 119 | --> :! 120 | $ ls -1 ~/.cache/samarium | wc -l 121 | 7 122 | ``` 123 | 124 | 125 | ## `help [section]` 126 | > Aliases: `?`, `h` 127 | 128 | Shows the help message. 129 | ``` 130 | --> :help 131 | ?|h|help shows this message 132 | exit|q|quit saves the session and quits the repl 133 | !|exit!|q!|quit! force quits the repl 134 | session manages sessions, see :? session for details 135 | clear clears the screen 136 | color changes the prompt color, see :? color for details 137 | debug toggles debug mode 138 | restore restores the previous session 139 | t|time times the execution of the following statement 140 | undo undoes the last statement 141 | ``` 142 | 143 | 144 | ## `time ` 145 | > Aliases: `t` 146 | 147 | Runs a given piece of code and shows how much time it took to execute. 148 | ``` 149 | --> :t <-math.is_prime(/?!("2137")) 150 | 1 151 | 0.009 seconds 152 | ``` 153 | 154 | 155 | ## `undo` 156 | Undoes the last statement. 157 | ``` 158 | --> x: /\/\! 159 | 10 160 | --> x+++: 161 | --> x 162 | 100 163 | --> :undo 164 | --> x 165 | 10 166 | ``` 167 | This is done by rerunning all inputs except for the last one (simple value 168 | lookups like `x` on lines 4 and 7 are excluded) and silencing all writes to 169 | stdout/stderr. 170 | 171 | 172 | ## Sessions 173 | 174 | Sessions provide a convenient way to save and restore your REPL state, allowing 175 | you to pick up right where you left off. 176 | 177 | 178 | ## `autosave [true|false]` 179 | Specifies whether sessions should be automatically saved on exit. Providing no 180 | argument will display the current value. 181 | ``` 182 | --> :session autosave false 183 | Autosave disabled 184 | --> :session autosave 185 | Autosave disabled 186 | ``` 187 | 188 | 189 | ## `delete-all` 190 | Deletes all saved sessions (both named and unnamed). 191 | ``` 192 | --> :session delete-all 193 | Are you sure you want to delete all 27 sessions? (Y/n) 194 | Removed 1.7KB of session files 195 | ``` 196 | 197 | 198 | ## `lifetime [time]` 199 | Specifies the lifetime (in days) for unnamed sessions. Sessions that are too old 200 | get removed on the next launch of the REPL. Providing no argument will display 201 | the current value. 202 | ``` 203 | --> :session lifetime 204 | Current session lifetime is 30 days 205 | --> :session lifetime 20 206 | Updated session lifetime to 20 days 207 | ``` 208 | 209 | 210 | ## `list` 211 | Displays a list of saved sessions and their sizes. 212 | ``` 213 | --> :session list 214 | 20230702193813.json (57.0B) 215 | 20230702163714.json (57.0B) 216 | 20230702194010.json (57.0B) 217 | 20230702163535.json (64.0B) 218 | 20230702193747.json (57.0B) 219 | 20230702210821.json (51.0B) 220 | 20230702170234.json (57.0B) 221 | prime-sieve.json (2.7KB) 222 | 20230702171025.json (57.0B) 223 | 20230702193958.json (57.0B) 224 | 20230702171009.json (57.0B) 225 | 20230702193902.json (57.0B) 226 | 20230702192942.json (57.0B) 227 | ---------------------------------------------- 228 | Total 3.3KB 229 | ``` 230 | 231 | 232 | ## `load ` 233 | Loads a given session. 234 | ``` 235 | --> :session load sorting 236 | --> quick_sort([/////?? ... _ ->? <<../\/\\>>]) 237 | [1, 2, 2, 3, 3, 4, 5, 11, 13, 14, 17, 18, 20, 20, 21, 21, 23, 24, 26, 29] 238 | ``` 239 | 240 | 241 | ## `restore` 242 | Loads the most recent unnamed session. 243 | ``` 244 | --> :session autosave 245 | Autosave enabled 246 | --> f * { ("!"++(///+++))! } 247 | --> :q 248 | $ samarium 249 | Samarium 0.6.2 250 | --> :session restore 251 | --> f() 252 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 253 | ``` 254 | 255 | 256 | ## `save [name]` 257 | Saves a sesssion under a given name. Becomes an unnamed session if no name is 258 | supplied. Session names can consist of English letters, digits, hyphens, and 259 | underscores. 260 | ``` 261 | --> pi * { * <-math.TAU--; } 262 | --> :session save pi 263 | --> :q 264 | $ samarium 265 | Samarium 0.6.2 266 | --> :session load pi 267 | --> pi() 268 | 3.141592653589793 269 | ``` 270 | 271 | 272 | [Dahlia codes]: https://github.com/dahlia-lib/spec/blob/main/SPECIFICATION.md#standard-formatting -------------------------------------------------------------------------------- /docs/variables.md: -------------------------------------------------------------------------------- 1 | # Variables 2 | 3 | Variables are defined using the assignment operator `:`, like so: 4 | ```sm 5 | my_var: /; 6 | ``` 7 | Variables can have many types, such as numbers, strings, arrays, tables, 8 | slices, or null. Functions and classes may also be treated as first-class 9 | variables. Only letters, numbers, and underscores can be used 10 | for variable names (case sensitive). 11 | 12 | !!! note 13 | Samarium follows the same naming convention as Python, i.e.:
14 | — snake_case for variables and functions
15 | — PascalCase for classes and type aliases
16 | — flatcase for modules 17 | 18 | Variables can be made private by prefixing the name with `#`, making them 19 | inaccessible to external modules. Private variable names don't collide with 20 | public variable names: 21 | ```sm 22 | var: -/; 23 | #var: /; 24 | 25 | var!; == -1 26 | #var!; == 1 27 | ``` 28 | 29 | 30 | ## Parallel assignment 31 | 32 | Multiple variables can be set at once by separating them with a comma: 33 | ```sm 34 | a, b: /, //; 35 | == same as 36 | a: /; 37 | b: //; 38 | 39 | 40 | primes: [/\, //, /\/, ///, /\//]; 41 | 42 | first, **rest, last: primes; 43 | == ^ collect as many values as possible 44 | == same as 45 | first: primes<<\>>; 46 | rest: primes<>; 47 | last: primes<<-/>>; 48 | ``` -------------------------------------------------------------------------------- /docs/zip.md: -------------------------------------------------------------------------------- 1 | # Zipping 2 | 3 | Samarium's zip operator `><` allow for easy zipping, that is, iterating through 4 | multiple iterables at once: 5 | ```sm 6 | names: ["Alice", "Bob", "Charlie"]; 7 | ages: [/\\\\, /\\\/, ////]; 8 | 9 | ... i ->? names >< ages { 10 | "$0 is $1 years old" --- i!; 11 | } 12 | ``` 13 | ``` 14 | Alice is 16 years old 15 | Bob is 17 years old 16 | Charlie is 15 years old 17 | ``` 18 | 19 | Enumerating iterables can be simulated by using an empty slice object: 20 | ```sm 21 | x: ["Alpha", "Beta", "Gamma"]; 22 | 23 | ... i, v ->? <<>> >< x { 24 | "x<<$0>> :: $1" --- [i, v]!; 25 | } 26 | ``` 27 | ``` 28 | x<<0>> :: Alpha 29 | x<<1>> :: Beta 30 | x<<2>> :: Gamma 31 | ``` 32 |
33 | 34 | ```sm 35 | <=string.ordinal; 36 | 37 | winners: ["Jake", "Clarisse", "Matt"]; 38 | 39 | ... p, w ->? <> >< winners { 40 | "$0 place: $1" --- [ordinal(p), w]; 41 | } 42 | ``` 43 | ``` 44 | 1st place: Jake 45 | 2nd place: Clarisse 46 | 3rd place: Matt 47 | ``` 48 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | [private] 2 | default: 3 | @just --list 4 | 5 | set positional-arguments 6 | 7 | fmt: 8 | poetry run ruff check --select=I --fix 9 | poetry run ruff format 10 | 11 | ruff: 12 | poetry run ruff check 13 | poetry run ruff format --check 14 | 15 | mypy: 16 | poetry run mypy samarium 17 | 18 | @run *args: 19 | poetry run samarium $@ 20 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Samarium Documentation 2 | copyright: Copyright © 2021–2024 trag1c 3 | repo_url: https://github.com/samarium-lang/Samarium 4 | repo_name: samarium-lang/Samarium 5 | nav: 6 | - Home: index.md 7 | - Examples: examples.md 8 | - Language Guide: 9 | - Numbers: numbers.md 10 | - Variables: variables.md 11 | - Operators: operators.md 12 | - Strings: strings.md 13 | - Comments: comments.md 14 | - Arrays: arrays.md 15 | - Tables: tables.md 16 | - Built-in Functions: builtins.md 17 | - Control Flow: controlflow.md 18 | - Null: null.md 19 | - Slices: slices.md 20 | - Zipping: zip.md 21 | - Enums: enums.md 22 | - Functions: functions.md 23 | - Modules: modules.md 24 | - Classes: classes.md 25 | - File I/O: fileio.md 26 | - Python Interop: interop.md 27 | - Standard Library: 28 | - collections module: stdcollections.md 29 | - datetime module: stddatetime.md 30 | - io module: stdio.md 31 | - iter module: stditer.md 32 | - math module: stdmath.md 33 | - operator module: stdoperator.md 34 | - random module: stdrandom.md 35 | - string module: stdstring.md 36 | - types module: stdtypes.md 37 | - Tools: tools.md 38 | theme: 39 | name: material 40 | logo: assets/icon-white.png 41 | favicon: assets/icon-black.png 42 | palette: 43 | - media: "(prefers-color-scheme: light)" 44 | scheme: default 45 | primary: black 46 | toggle: 47 | icon: material/toggle-switch 48 | name: Switch to dark mode 49 | - media: "(prefers-color-scheme: dark)" 50 | scheme: slate 51 | primary: black 52 | toggle: 53 | icon: material/toggle-switch-off-outline 54 | name: Switch to light mode 55 | extra_javascript: 56 | - https://unpkg.com/shiki@0.10.1/dist/index.unpkg.iife.js 57 | - assets/script.js 58 | extra_css: 59 | - assets/style.css 60 | markdown_extensions: 61 | - admonition 62 | - pymdownx.superfences: 63 | custom_fences: 64 | - name: sm 65 | class: sm 66 | - footnotes 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "samarium" 3 | version = "0.6.2" 4 | description = "The Samarium Programming Language" 5 | authors = ["trag1c "] 6 | license = "MIT" 7 | documentation = "https://samarium-lang.github.io/Samarium/" 8 | repository = "https://github.com/samarium-lang/Samarium" 9 | readme = "README.md" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.9" 13 | dahlia = "^3.0.0" 14 | crossandra = "^2.2.1" 15 | 16 | [tool.poetry.group.dev.dependencies] 17 | ruff = "^0.4.8" 18 | mkdocs = "^1.5.1" 19 | mkdocs-material = "^9.1.21" 20 | mypy = "^1.10.0" 21 | mike = "^2.1.1" 22 | 23 | [tool.ruff.lint] 24 | select = ["ALL"] 25 | ignore = ["COM", "D", "DTZ", "FIX", "S", "SLF", "ANN1", "ANN401", "C90", "ISC001", "T201", "TD003", "PLR2004", "PLR091", "PERF203", "PYI034"] 26 | 27 | [tool.ruff.lint.pylint] 28 | max-returns = 8 29 | 30 | [tool.mypy] 31 | disable_error_code = "attr-defined, override" 32 | 33 | [tool.poetry.scripts] 34 | samarium = "samarium.__main__:main" 35 | samarium-debug = "samarium.__init__:main_debug" 36 | 37 | [build-system] 38 | requires = ["poetry-core>=1.0.0"] 39 | build-backend = "poetry.core.masonry.api" 40 | 41 | -------------------------------------------------------------------------------- /samarium/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import nullcontext, suppress 3 | from pathlib import Path 4 | 5 | from samarium.core import run 6 | from samarium.exceptions import DAHLIA 7 | from samarium.repl import REPL, Command 8 | from samarium.transpiler import Registry 9 | from samarium.utils import __version__ 10 | 11 | OPTIONS = ("-v", "--version", "-c", "--command", "-h", "--help") 12 | 13 | HELP = """samarium &7[option] [-c cmd | file]&R 14 | options and arguments:\n""" + "\n".join( 15 | map( 16 | str, 17 | ( 18 | Command( 19 | "-c", 20 | "--command", 21 | arg="", 22 | sep=", ", 23 | msg="reads program from string", 24 | ), 25 | Command("-h", "--help", sep=", ", msg="shows this message"), 26 | Command("-v", "--version", sep=", ", msg="prints Samarium version"), 27 | Command(arg="file", sep=", ", msg=" reads program from script file"), 28 | ), 29 | ) 30 | ) 31 | 32 | 33 | def main(*, debug: bool = False) -> None: 34 | reg = Registry(globals()) 35 | 36 | if len(sys.argv) == 1: 37 | return REPL(debug=debug).run() 38 | 39 | if (arg := sys.argv[1]) in OPTIONS: 40 | if arg in OPTIONS[:2]: 41 | print(f"Samarium {__version__}") 42 | elif arg in OPTIONS[2:4]: 43 | if len(sys.argv) > 2: 44 | run(sys.argv[2] + " !", reg, arg, debug=debug) 45 | DAHLIA.print("&4missing code to execute", file=sys.stderr) 46 | elif arg in OPTIONS[4:]: 47 | DAHLIA.print(HELP) 48 | sys.exit() 49 | 50 | try: 51 | file = Path(arg).read_text() 52 | except OSError: 53 | DAHLIA.print(f"&4file not found: {arg}", file=sys.stderr) 54 | else: 55 | with nullcontext() if debug else suppress(Exception, KeyboardInterrupt): 56 | file = "\n".join(file.splitlines()[file.startswith("#!") :]) 57 | run(file, reg, arg, debug=debug) 58 | 59 | 60 | def main_debug() -> None: 61 | main(debug=True) 62 | -------------------------------------------------------------------------------- /samarium/__main__.py: -------------------------------------------------------------------------------- 1 | from samarium import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /samarium/builtins.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime, timezone 4 | from re import compile 5 | from time import sleep as _sleep 6 | from time import time_ns 7 | from types import GeneratorType 8 | from typing import TYPE_CHECKING, Any, TypeVar 9 | 10 | from samarium.classes import ( 11 | MISSING, 12 | NULL, 13 | Array, 14 | Attrs, 15 | Null, 16 | Num, 17 | Number, 18 | Slice, 19 | String, 20 | correct_type, 21 | ) 22 | from samarium.exceptions import ( 23 | SamariumError, 24 | SamariumIOError, 25 | SamariumSyntaxError, 26 | SamariumTypeError, 27 | ) 28 | 29 | if TYPE_CHECKING: 30 | from collections.abc import Callable 31 | 32 | MISSING_ARGS_PATTERN = compile( 33 | r"\w+\(\) takes exactly one argument \(0 given\)" 34 | r"|\w+\(\) missing (\d+) required positional argument" 35 | ) 36 | 37 | NULL_STRING = String() 38 | 39 | T = TypeVar("T") 40 | 41 | TOO_MANY_ARGS_PATTERN = compile( 42 | r"\w+\(\) takes (\d+) positional arguments? but (\d+) (?:was|were) given" 43 | ) 44 | 45 | 46 | def dtnow() -> Array: 47 | utcnow = datetime.now(tz=timezone.utc) 48 | now = datetime.now().timetuple() 49 | utcnow_tt = utcnow.timetuple() 50 | tz = now[3] - utcnow_tt[3], now[4] - utcnow_tt[4] 51 | utcnow_tpl = utcnow_tt[:-3] + (utcnow.microsecond // 1000,) + tz 52 | return Array(map(Num, utcnow_tpl)) 53 | 54 | 55 | def mkslice(start: Any = None, stop: Any = MISSING, step: Any = None) -> Any: 56 | if stop is MISSING: 57 | if start is not None: 58 | return start 59 | return Slice(NULL) 60 | start = NULL if start is None else start 61 | stop = NULL if stop is None else stop 62 | step = NULL if step is None else step 63 | return Slice(start, stop, step) 64 | 65 | 66 | def print_safe(*args: Attrs | Callable[..., Any] | bool | None) -> Attrs: 67 | typechecked_args = list(map(correct_type, args)) 68 | return_args = typechecked_args.copy() 69 | strs = list(map(str, typechecked_args)) 70 | types = list(map(type, typechecked_args)) 71 | if tuple in types: 72 | msg = "missing brackets" 73 | raise SamariumSyntaxError(msg) 74 | if GeneratorType in types: 75 | msg = "invalid comprehension" 76 | raise SamariumSyntaxError(msg) 77 | print(*strs) 78 | if len(return_args) > 1: 79 | return Array(return_args) 80 | if not return_args or types[0] is Null: 81 | return NULL 82 | return return_args[0] # type: ignore[return-value] 83 | 84 | 85 | def readline(prompt: String = NULL_STRING) -> String: 86 | try: 87 | return String(input(prompt.val)) 88 | except KeyboardInterrupt: 89 | msg = "^C" 90 | except EOFError: 91 | msg = "^D" 92 | raise SamariumIOError(msg) 93 | 94 | 95 | def sleep(*args: Number) -> None: 96 | if not args: 97 | msg = "no argument provided for ,.," 98 | raise SamariumTypeError(msg) 99 | if len(args) > 1: 100 | msg = ",., only takes one argument" 101 | raise SamariumTypeError(msg) 102 | if not isinstance((time := args[0]), Number): 103 | msg = ",., only accepts integers" 104 | raise SamariumTypeError(msg) 105 | _sleep(time.val / 1000) 106 | 107 | 108 | def t(obj: T | None = None) -> T | None: 109 | return obj 110 | 111 | 112 | def throw(obj: String | Array[Any] = NULL_STRING) -> None: 113 | note = "" 114 | if isinstance(obj, Array): 115 | if len(obj.val) != 2: 116 | msg = "array must be of form [error_msg, note]" 117 | raise SamariumTypeError(msg) 118 | error_msg, note = obj.val 119 | if not isinstance(error_msg, String): 120 | msg = "error message must be a string" 121 | raise SamariumTypeError(msg) 122 | if not isinstance(note, String): 123 | msg = "error note must be a string" 124 | raise SamariumTypeError(msg) 125 | note = f"\n&1[Note] {note.val}" 126 | elif isinstance(obj, String): 127 | error_msg = obj 128 | else: 129 | msg = "throw argument must be a string or a 2-element array" 130 | raise SamariumTypeError(msg) 131 | raise SamariumError(error_msg.val + note) 132 | 133 | 134 | def timestamp() -> Number: 135 | return Num(time_ns() // 1_000_000) 136 | -------------------------------------------------------------------------------- /samarium/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from samarium.classes.base import ( 4 | MISSING, 5 | NEXT, 6 | NULL, 7 | Array, 8 | Attrs, 9 | Dataclass, 10 | Enum, 11 | Function, 12 | Iterator, 13 | Module, 14 | Null, 15 | Num, 16 | Number, 17 | Slice, 18 | String, 19 | Table, 20 | Type, 21 | UserAttrs, 22 | Zip, 23 | correct_type, 24 | ) 25 | from samarium.classes.fileio import File, FileManager, Mode 26 | 27 | __all__ = ( 28 | "Array", 29 | "Attrs", 30 | "Dataclass", 31 | "Enum", 32 | "File", 33 | "Function", 34 | "Iterator", 35 | "Slice", 36 | "Table", 37 | "Number", 38 | "Num", 39 | "String", 40 | "Module", 41 | "Type", 42 | "MISSING", 43 | "NEXT", 44 | "NULL", 45 | "Null", 46 | "FileManager", 47 | "Mode", 48 | "UserAttrs", 49 | "Zip", 50 | "correct_type", 51 | ) 52 | -------------------------------------------------------------------------------- /samarium/classes/fileio.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from os import write 5 | from pathlib import Path 6 | from typing import IO, TYPE_CHECKING, Any, cast 7 | 8 | from samarium.classes.base import NULL, Array, Attrs, Null, Num, Number, Slice, String 9 | from samarium.exceptions import SamariumIOError, SamariumTypeError, SamariumValueError 10 | from samarium.utils import get_type_name 11 | 12 | if TYPE_CHECKING: 13 | from collections.abc import Iterator 14 | 15 | 16 | class Mode(Enum): 17 | READ = "r" 18 | WRITE = "w" 19 | READ_WRITE = "r+" 20 | APPEND = "a" 21 | 22 | 23 | class FileManager: 24 | @staticmethod 25 | def create(path: String) -> Null: 26 | Path(path.val).touch() 27 | return NULL 28 | 29 | @staticmethod 30 | def open(path: String | Number, mode: Mode, *, binary: bool = False) -> File: 31 | if isinstance(path, Number): 32 | if mode is Mode.READ_WRITE: 33 | msg = "cannot open a standard stream in a read & write mode" 34 | raise SamariumIOError(msg) 35 | if not path.is_int: 36 | msg = "cannot use non-integers" 37 | raise SamariumValueError(msg) 38 | pth = cast("str | int", path.val) 39 | if isinstance(pth, str): 40 | f = Path(pth).open(mode.value + "b" * binary) # noqa: SIM115 41 | else: 42 | f = open(pth, mode.value + "b" * binary) # noqa: PTH123, SIM115 43 | return File(f, mode.name, pth, binary=binary) 44 | 45 | @staticmethod 46 | def open_binary(path: String, mode: Mode) -> File: 47 | return FileManager.open(path, mode, binary=True) 48 | 49 | @staticmethod 50 | def quick( 51 | path: String | File | Number, 52 | mode: Mode, 53 | *, 54 | data: String | Array | None = None, 55 | binary: bool = False, 56 | ) -> String | Array | Null: 57 | if isinstance(path, String): 58 | p = Path(path.val) 59 | if mode is Mode.READ: 60 | content = p.read_bytes() if binary else p.read_text() 61 | return Array(map(Num, content)) if binary else String(content) 62 | if data is None: 63 | msg = "missing data" 64 | raise SamariumIOError(msg) 65 | if not isinstance(data, Array): 66 | p.write_text(data.val) 67 | return NULL 68 | if isinstance(data, Array): 69 | bytes_ = b"" 70 | for i in data.val: 71 | try: 72 | bytes_ += i.val.to_bytes(1, "big") 73 | except AssertionError: # TODO(trag1c): why AssertionError? 74 | msg = "some items in the array are not of type Integer" 75 | raise SamariumTypeError(msg) from None 76 | p.write_bytes(bytes_) 77 | elif isinstance(path, Number): 78 | if not path.is_int: 79 | msg = "cannot use non-integers" 80 | raise SamariumValueError(msg) 81 | if mode in {Mode.APPEND, Mode.WRITE}: 82 | fd = cast(int, path.val) 83 | write(fd, str(data).encode()) 84 | else: 85 | msg = ( 86 | "reading from file descriptors is " 87 | "not supported for quick operations" 88 | ) 89 | raise SamariumIOError(msg) 90 | else: 91 | file = path 92 | if mode is Mode.READ: 93 | return file.load() 94 | if data is not None: 95 | file.save(data) 96 | else: 97 | msg = "missing data" 98 | raise SamariumIOError(msg) 99 | return NULL 100 | 101 | 102 | class File(Attrs): 103 | __slots__ = ("binary", "mode", "path", "val") 104 | 105 | def __init__(self, file: IO, mode: str, path: str | int, *, binary: bool) -> None: 106 | self.binary = binary 107 | self.mode = mode 108 | self.path = path 109 | self.val = file 110 | 111 | def __bool__(self) -> bool: 112 | return not self.val.closed 113 | 114 | def __str__(self) -> str: 115 | return f'File(path:"{self.path}", mode:{self.mode})' 116 | 117 | def __invert__(self) -> Null: 118 | self.val.close() 119 | return NULL 120 | 121 | def __iter__(self) -> Iterator[String] | Iterator[Array[Number]]: 122 | if self.binary: 123 | for line in self.val: 124 | yield Array(map(Num, list(line))) 125 | else: 126 | yield from map(String, self.val) 127 | 128 | def __getitem__(self, index: Any) -> Array | String | Number | Null: 129 | if isinstance(index, Slice): 130 | if index.is_empty(): 131 | return Num(self.val.tell()) 132 | if isinstance(index.step, Number): 133 | msg = "cannot use step" 134 | raise SamariumIOError(msg) 135 | if isinstance(index.start, Number): 136 | if not isinstance(index.stop, Number): 137 | return self.load(index.start) 138 | if not (index.start.is_int or index.stop.is_int): 139 | msg = f"invalid index: {index}" 140 | raise SamariumValueError(msg) 141 | start = cast(int, index.start.val) 142 | stop = cast(int, index.stop.val) 143 | current_pos = self.val.tell() 144 | self.val.seek(start) 145 | data = self.val.read(stop - start) 146 | self.val.seek(current_pos) 147 | if self.binary: 148 | if isinstance(data, bytes): 149 | data = [*data] 150 | return Array(map(Num, data)) 151 | return String(data) 152 | return self[Slice(Num(0), slice.stop, slice.step)] 153 | 154 | self.val.seek(index.val) 155 | return NULL 156 | 157 | def load(self, bytes_: Number | None = None) -> String | Array: 158 | if bytes_ is None: 159 | bytes_ = Num(-1) 160 | if not bytes_.is_int: 161 | msg = "cannot use non-integers" 162 | raise SamariumValueError(msg) 163 | val = self.val.read(cast(int, bytes_.val)) 164 | if self.binary: 165 | return Array(map(Num, val)) 166 | return String(val) 167 | 168 | def save(self, data: String | Array) -> Null: 169 | if (self.binary and isinstance(data, String)) or ( 170 | not self.binary and isinstance(data, Array) 171 | ): 172 | raise SamariumTypeError(get_type_name(data)) 173 | if isinstance(data, Array): 174 | bytes_ = b"" 175 | for i in data.val: 176 | try: 177 | bytes_ += i.val.to_bytes(1, "big") 178 | except AssertionError: 179 | msg = "some items in the array are not of type Integer" 180 | raise SamariumTypeError(msg) from None 181 | self.val.write(bytes_) 182 | else: 183 | self.val.write(data.val) 184 | return NULL 185 | -------------------------------------------------------------------------------- /samarium/core.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: F401 2 | from __future__ import annotations 3 | 4 | import ast 5 | import importlib.machinery 6 | import importlib.util 7 | import sys 8 | from pathlib import Path 9 | from typing import TYPE_CHECKING 10 | 11 | from samarium import exceptions as exc 12 | from samarium.builtins import ( 13 | dtnow, 14 | mkslice, 15 | print_safe, 16 | readline, 17 | sleep, 18 | t, 19 | throw, 20 | timestamp, 21 | ) 22 | from samarium.classes import ( 23 | MISSING, 24 | NEXT, 25 | NULL, 26 | Array, 27 | Dataclass, 28 | Enum, 29 | FileManager, 30 | Function, 31 | Mode, 32 | Module, 33 | Null, 34 | Num, 35 | Number, 36 | String, 37 | Table, 38 | UserAttrs, 39 | correct_type, 40 | ) 41 | from samarium.exceptions import DAHLIA 42 | from samarium.imports import merge_objects, parse_string, resolve_path 43 | from samarium.runtime import Runtime 44 | from samarium.tokenizer import tokenize 45 | from samarium.transpiler import Registry, Transpiler 46 | from samarium.utils import sysexit 47 | 48 | if TYPE_CHECKING: 49 | from samarium.classes import Attrs 50 | 51 | 52 | def import_to_scope(data: str, reg: Registry, source: str) -> None: 53 | modules = parse_string(data) 54 | for mod in modules: 55 | if mod.name == "samarium": 56 | raise exc.SamariumRecursionError 57 | path = resolve_path(mod.name, source) 58 | mod_path = (path / f"{mod.name}.sm").resolve() 59 | if mod_path.exists(): 60 | imported = run(mod_path.read_text(), Registry({}), mod_path) 61 | else: 62 | spec = importlib.util.spec_from_file_location( 63 | mod.name, str(path / f"{mod.name}.py") 64 | ) 65 | if spec is None: 66 | msg = "couldn't load spec" 67 | raise ValueError(msg) 68 | module = importlib.util.module_from_spec(spec) 69 | sys.modules[mod.name] = module 70 | if spec.loader is None: 71 | msg = "ModuleSpec.loader is None" 72 | raise ValueError(msg) 73 | spec.loader.exec_module(module) 74 | registry = { 75 | f"sm_{k}": v 76 | for k, v in vars(module).items() 77 | if getattr(v, "__pyexported__", False) 78 | } 79 | imported = Registry(registry) 80 | 81 | reg.vars.update(merge_objects(reg, imported, mod)) 82 | 83 | 84 | def import_inline(data: str, source: str) -> Attrs: 85 | reg = Registry({}) 86 | import_to_scope(data, reg, source) 87 | return reg.vars.popitem()[1] 88 | 89 | 90 | def run( 91 | code: str, 92 | reg: Registry, 93 | source: Path | str, 94 | *, 95 | debug: bool = False, 96 | load_template: bool = True, 97 | repl: bool = False, 98 | ) -> Registry: 99 | runtime_state = Runtime.repl 100 | Runtime.repl = repl 101 | code = Transpiler(tokenize(code), reg).transpile().output 102 | if load_template: 103 | code = ( 104 | (Path(__file__).resolve().parent / "template.txt") 105 | .read_text() 106 | .replace("{{CODE}}", code) 107 | .replace( 108 | "{{SOURCE}}", 109 | str(Path(source).resolve() if isinstance(source, str) else source), 110 | ) 111 | ) 112 | try: 113 | if debug: 114 | code = ast.unparse(ast.parse(code)) 115 | DAHLIA.print(f"&j{code}", file=sys.stderr) 116 | reg.vars = globals() | reg.vars 117 | if repl: 118 | try: 119 | res = eval(code, reg.vars) 120 | if not (res is None or res is NULL): 121 | print(repr(res)) 122 | except SyntaxError: 123 | exec(code, reg.vars) 124 | else: 125 | exec(code, reg.vars) 126 | except Exception as e: # noqa: BLE001 127 | exc.handle_exception(e) 128 | Runtime.repl = runtime_state 129 | return reg 130 | -------------------------------------------------------------------------------- /samarium/exceptions.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from re import compile 3 | 4 | from dahlia import Dahlia 5 | 6 | from samarium.runtime import Runtime 7 | 8 | DAHLIA = Dahlia() 9 | NDE_TYPES = {AttributeError, NameError, UnboundLocalError} 10 | 11 | ARG_NOT_ITER = compile(r"argument of type '(\w+)' is not iterable") 12 | BAD_OP = compile( 13 | r"'(.+)' not supported between instances of '(\w+)' and '(\w+)'" 14 | r"|unsupported operand type\(s\) for (.+): '(\w+)' and '(\w+)'" 15 | ) 16 | BAD_UOP = compile(r"bad operand type for unary (.+): '(\w+)'") 17 | NO_GETITEM = compile(r"'(\w+)' object is not subscriptable") 18 | NO_SETITEM = compile(r"'(\w+)' object does not support item assignment") 19 | NOT_CALLITER = compile(r"'(\w+)' object is not (\w+)") 20 | OP_MAP = { 21 | ">=": ">:", 22 | "<=": "<:", 23 | "/": "--", 24 | "%": "---", 25 | "*": "++", 26 | "@": "><", 27 | "** or pow()": "+++", 28 | } 29 | SINGLE_QUOTED_NAME = compile(r"'(\w+)'") 30 | 31 | 32 | def clear_name(name: str) -> str: 33 | return name.removeprefix("__").removeprefix("sm_") 34 | 35 | 36 | def handle_exception(exception: Exception) -> None: 37 | exc_type = type(exception) 38 | errmsg = str(exception) 39 | name = "" 40 | if exc_type is NotDefinedError: 41 | exception = NotDefinedError(".".join(map(clear_name, errmsg.split(".")))) 42 | if ( 43 | exc_type is TypeError 44 | and "missing 1 required positional argument: 'self'" in errmsg 45 | ): 46 | exception = SamariumTypeError("missing instance") 47 | elif m := BAD_OP.match(errmsg): 48 | op, lhs, rhs = filter(None, m.groups()) 49 | op = OP_MAP.get(op, op) 50 | exc_type = NotDefinedError 51 | exception = exc_type(f"{clear_name(lhs)} {op} {clear_name(rhs)}") 52 | elif m := BAD_UOP.match(errmsg): 53 | op, type_ = m.groups() 54 | exc_type = NotDefinedError 55 | exception = exc_type(f"{op}{clear_name(type_)}") 56 | elif m := ARG_NOT_ITER.match(errmsg): 57 | exc_type = NotDefinedError 58 | type_ = clear_name(m.group(1)) 59 | exception = exc_type(f"->? {type_}") 60 | elif m := NO_GETITEM.match(errmsg): 61 | exc_type = NotDefinedError 62 | type_ = clear_name(m.group(1)) 63 | exception = exc_type(f"{type_}<<>>") 64 | elif m := NO_SETITEM.match(errmsg): 65 | exc_type = NotDefinedError 66 | type_ = clear_name(m.group(1)) 67 | exception = exc_type(f"{type_}<<>>:") 68 | elif m := NOT_CALLITER.match(errmsg): 69 | exc_type = NotDefinedError 70 | type_ = clear_name(m.group(1)) 71 | template = "... _ ->? {}" if m.group(2) == "iterable" else "{}()" 72 | exception = exc_type(template.format(type_)) 73 | elif exc_type is SyntaxError: 74 | exception = SamariumSyntaxError( 75 | f"invalid syntax at {int(errmsg.split()[-1][:-1])}" 76 | ) 77 | elif exc_type in NDE_TYPES: 78 | names = SINGLE_QUOTED_NAME.findall(errmsg) 79 | exc_type = NotDefinedError 80 | exception = exc_type( 81 | f"{clear_name(names[0])}<<>>{':' * (names[1] == '__setitem__')}" 82 | if len(names) >= 2 and names[1] in {"__getitem__", "__setitem__"} 83 | else ".".join(map(clear_name, names)) 84 | ) 85 | elif exc_type is ZeroDivisionError: 86 | name = "MathError" 87 | elif exc_type not in {AssertionError, NotDefinedError}: 88 | name = exc_type.__name__.removeprefix("Samarium") 89 | name = name or exc_type.__name__ 90 | DAHLIA.print(f"&4[{name}] {exception}", file=sys.stderr) 91 | if not Runtime.repl: 92 | sys.exit(1) 93 | 94 | 95 | class SamariumError(Exception): 96 | pass 97 | 98 | 99 | class NotDefinedError(SamariumError): 100 | def __init__(self, inp: object, message: str = "") -> None: 101 | if isinstance(inp, str): 102 | message = inp 103 | inp = "" 104 | else: 105 | inp = type(inp).__name__ 106 | super().__init__(f"{inp}.{message}" if inp else message) 107 | 108 | 109 | class SamariumImportError(SamariumError): 110 | pass 111 | 112 | 113 | class SamariumSyntaxError(SamariumError): 114 | pass 115 | 116 | 117 | class SamariumTypeError(SamariumError): 118 | pass 119 | 120 | 121 | class SamariumValueError(SamariumError): 122 | pass 123 | 124 | 125 | class SamariumIOError(SamariumError): 126 | pass 127 | 128 | 129 | class SamariumRecursionError(SamariumError): 130 | pass 131 | -------------------------------------------------------------------------------- /samarium/imports.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | from pathlib import Path 6 | from re import Pattern, compile, sub 7 | from typing import TYPE_CHECKING 8 | 9 | from samarium.classes import Attrs, Module 10 | from samarium.exceptions import SamariumImportError, SamariumSyntaxError 11 | 12 | if TYPE_CHECKING: 13 | from samarium.transpiler import Registry 14 | 15 | 16 | FORMATTERS = { 17 | r"\bsm_(\w+)\b": r"\g<1>", 18 | r"\.?Array\(\[": ".[", 19 | r"\]\)": "]", 20 | } 21 | 22 | MODULE_NAMES = [ 23 | "collections", 24 | "datetime", 25 | "io", 26 | "iter", 27 | "math", 28 | "operator", 29 | "random", 30 | "string", 31 | "types", 32 | ] 33 | 34 | 35 | def parse_string(string: str) -> list[Mod]: 36 | string = format_string(string) 37 | for it in Import.__members__.values(): 38 | imptype, pattern = it, it.value 39 | if pattern.match(string): 40 | break 41 | else: 42 | msg = "invalid import syntax" 43 | raise SamariumSyntaxError(msg) 44 | if imptype is Import.MODULE: 45 | mods = string.split(",") 46 | out = [] 47 | for m in mods: 48 | name, _, alias = m.partition(":") 49 | out.append(Mod(name, alias or None)) 50 | return out 51 | if imptype is Import.STAR: 52 | return [Mod(string.split(".")[0], None, objects=True)] 53 | if imptype is Import.OBJECT: 54 | mod, obj = string.split(".") 55 | obj, _, alias = obj.partition(":") 56 | return [Mod(mod, None, [Obj(obj, alias or None)])] 57 | mod, objects_ = string.split(".") 58 | objects = objects_.strip("[]").split(",") 59 | objs = [] 60 | for o in objects: 61 | name, _, alias = o.partition(":") 62 | objs.append(Obj(name, alias or None)) 63 | return [Mod(mod, None, objs)] 64 | 65 | 66 | def format_string(string: str) -> str: 67 | for pattern, repl in FORMATTERS.items(): 68 | string = sub(pattern, repl, string) 69 | return string 70 | 71 | 72 | def merge_objects(reg: Registry, imported: Registry, module: Mod) -> dict[str, Attrs]: 73 | vars_ = reg.vars.copy() 74 | if module.objects is False: 75 | return vars_ | {f"sm_{module.alias}": Module(module.name, imported.vars)} 76 | if module.objects is True: 77 | return vars_ | {k: v for k, v in imported.vars.items() if k.startswith("sm_")} 78 | for obj in module.objects: 79 | try: 80 | vars_[f"sm_{obj.alias}"] = imported.vars[f"sm_{obj.name}"] 81 | except KeyError: 82 | msg = f"{obj.name} is not a member of the {module.name} module" 83 | raise SamariumImportError(msg) from None 84 | return vars_ 85 | 86 | 87 | def regex(string: str) -> Pattern: 88 | return compile("^" + string.replace("W", r"\w+") + "$") 89 | 90 | 91 | def resolve_path(name: str, source: str) -> Path: 92 | try: 93 | path = Path(source).parent 94 | except IndexError: # REPL 95 | path = Path.cwd() 96 | paths = [e.name for e in path.iterdir()] 97 | if not (f"{name}.sm" in paths or f"{name}.py" in paths): 98 | if name not in MODULE_NAMES: 99 | msg = f"invalid module: {name}" 100 | raise SamariumImportError(msg) 101 | path = Path(__file__).resolve().parent / "modules" 102 | return path 103 | 104 | 105 | class Import(Enum): 106 | MODULE = regex(r"W(?::W)?(?:,W(?::W)?)*") 107 | STAR = regex(r"W\.\*") 108 | OBJECT = regex(r"W\.W(?::W)?") 109 | OBJECTS = regex(r"W\.\[W(?::W)?(?:,W(?::W)?)*\]") 110 | 111 | 112 | class ImportAttrs: 113 | name: str 114 | alias: str | None 115 | 116 | def __post_init__(self) -> None: 117 | if self.alias is None: 118 | self.alias = self.name 119 | 120 | 121 | @dataclass 122 | class Mod(ImportAttrs): 123 | name: str 124 | alias: str | None = None 125 | objects: list[Obj] | bool = False 126 | 127 | 128 | @dataclass 129 | class Obj(ImportAttrs): 130 | name: str 131 | alias: str | None = None 132 | -------------------------------------------------------------------------------- /samarium/modules/collections.sm: -------------------------------------------------------------------------------- 1 | <=operator; 2 | 3 | @ ArithmeticArray { 4 | => array * { 5 | 'array: array; 6 | } 7 | 8 | apply op other * { 9 | * ArithmeticArray([op(i, other) ... i ->? 'array]); 10 | } 11 | 12 | & other * { * 'apply(operator.and, other); } 13 | + other * { * 'apply(operator.add, other); } 14 | -- other * { * 'apply(operator.div, other); } 15 | :: other * { * 'apply(operator.eq, other); } 16 | >: other * { * 'apply(operator.ge, other); } 17 | > other * { * 'apply(operator.gt, other); } 18 | <: other * { * 'apply(operator.le, other); } 19 | < other * { * 'apply(operator.lt, other); } 20 | --- other * { * 'apply(operator.mod, other); } 21 | ++ other * { * 'apply(operator.mul, other); } 22 | ::: other * { * 'apply(operator.ne, other); } 23 | | other * { * 'apply(operator.or, other); } 24 | +++ other * { * 'apply(operator.pow, other); } 25 | - other * { * 'apply(operator.sub, other); } 26 | ^ other * { * 'apply(operator.xor, other); } 27 | 28 | <<>> item * { 29 | out: 'array<>; 30 | ? out?! :: <-types.Array { 31 | * ArithmeticArray(out); 32 | } 33 | * out; 34 | } 35 | 36 | <<>>: item value * { 'array<>: value; } 37 | 38 | $ * { * 'array$; } 39 | ? * { * / ? 'array ,, \; } 40 | ! * { * "A" + ""?!('array); } 41 | } 42 | 43 | @ Deque { 44 | 45 | => size? * { 46 | size <> -/; 47 | 'deque: []; 48 | 'size: size; 49 | } 50 | 51 | $ * { * 'deque$; } 52 | is_empty * { * ~~ '$; } 53 | is_full * { * '$ :: 'size; } 54 | 55 | throw_empty * { 56 | ? 'is_empty() { 57 | "deque is empty"!!!; 58 | } 59 | } 60 | 61 | throw_full * { 62 | ? 'is_full() { 63 | "deque is full (size " + ""?!('size) + ")"!!!; 64 | } 65 | } 66 | 67 | put_front item * { 68 | 'throw_full(); 'deque: [item] + 'deque; 69 | } 70 | 71 | put item * { 'throw_full(); 'deque+: [item]; } 72 | 73 | put_front_all items * { 74 | ... item ->? items { 75 | 'put_front(item); 76 | } 77 | } 78 | 79 | put_all items * { 80 | ... item ->? items { 81 | 'put(item); 82 | } 83 | } 84 | 85 | front * { * 'deque<<\>>; } 86 | back * { * 'deque<<-/>>; } 87 | ? * { * '$ > \; } 88 | 89 | get_front * { 90 | 'throw_empty(); 91 | out: 'deque<<\>>; 92 | 'deque-: \; 93 | * out; 94 | } 95 | 96 | get * { 97 | 'throw_empty(); 98 | out: 'deque<<-/>>; 99 | 'deque-: 'deque$-; 100 | * out; 101 | } 102 | 103 | ! * { 104 | <=string.wrap; 105 | <=types.String; 106 | ? '$ <: /\/ { 107 | items: ""?!('deque)<>; 108 | } ,, { 109 | front: ""?!('front()); 110 | back: ""?!('back()); 111 | ? 'front()?! :: String { 112 | front: wrap(front, "\""); 113 | } 114 | ? 'back()?! :: String { 115 | back: wrap(back, "\""); 116 | } 117 | items: ""?!(front) + ", ..., " + ""?!(back); 118 | } 119 | * "Deque(" + items + ")"; 120 | } 121 | } 122 | 123 | @ Queue { 124 | => size? * { 125 | size <> -/; 126 | 'queue: []; 127 | 'size: size; 128 | } 129 | 130 | $ * { * 'queue$; } 131 | ->? element * { * element ->? 'queue; } 132 | is_empty * { * ~~ '$; } 133 | is_full * { * '$ :: 'size; } 134 | 135 | throw_empty * { 136 | ? 'is_empty() { 137 | "queue is empty"!!!; 138 | } 139 | } 140 | 141 | put item * { 142 | ? 'is_full() { 143 | "queue is full (size " + ""?!('size) + ")"!!!; 144 | } 145 | 'queue+: [item]; 146 | } 147 | 148 | put_all items * { 149 | ... item ->? items { 150 | 'put(item); 151 | } 152 | } 153 | 154 | get * { 155 | 'throw_empty(); 156 | out: 'queue<<\>>; 157 | 'queue-: \; 158 | * out; 159 | } 160 | 161 | first * { 'throw_empty(); * 'queue<<\>>; } 162 | last * { 'throw_empty(); * 'queue<<-/>>; } 163 | ? * { * '$>; } 164 | 165 | ! * { 166 | <=string.wrap; 167 | <=types.String; 168 | ? '$ <: /\/ { 169 | items: ""?!('queue)<>; 170 | } ,, { 171 | first: ""?!('first()); 172 | last: ""?!('last()); 173 | ? 'first()?! :: String { 174 | first: wrap(first, "\""); 175 | } 176 | ? 'last()?! :: String { 177 | last: wrap(last, "\""); 178 | } 179 | items: ""?!(first) + ", ..., " + ""?!(last); 180 | } 181 | * "Queue(" + items + ")"; 182 | } 183 | } 184 | 185 | @ Set { 186 | => items? capacity? * { 187 | items <> []; 188 | 'items: -items; 189 | } 190 | 191 | $ * { * 'items$; } 192 | 193 | add value * { 194 | x: value ~~ ->? '; 195 | 'items|: [value]; 196 | * x; 197 | } 198 | 199 | ? * { * ~~ 'is_empty(); } 200 | 201 | ! * { 202 | * "Set(" + ""?!('items)<> + ")"; 203 | } 204 | 205 | remove value * { 'items-: [value]; } 206 | 207 | ->? value * { 208 | * <-iter.find('items, value)>:; 209 | } 210 | 211 | #check_type value * { 212 | ? value?! ::: Set { 213 | "expected Set, received " + ""?!(value?!)!!!; 214 | } 215 | } 216 | 217 | clear * { 'items: []; } 218 | is_empty * { * '$ :: \; } 219 | ::: other * { * ~~ ' :: other; } 220 | :: other * { 221 | sort: <-iter.sorted; 222 | * sort('items) :: sort(other.items); 223 | } 224 | 225 | | other * { 226 | '#check_type(other); 227 | * Set('items | other.items); 228 | } 229 | 230 | ^ other * { 231 | '#check_type(other); 232 | * Set('items ^ other.items); 233 | } 234 | 235 | & other * { 236 | '#check_type(other); 237 | * Set('items & other.items); 238 | } 239 | 240 | - other * { 241 | '#check_type(other); 242 | * Set('items -- other.items); 243 | } 244 | 245 | > other * { 246 | '#check_type(other); 247 | 248 | ? '$ <: other$ { * \; } 249 | 250 | ... value ->? other.items { 251 | ? value ~~ ->? ' { * \; } 252 | } 253 | * /; 254 | } 255 | 256 | >: other * { * ' :: other || ' > other; } 257 | } 258 | 259 | @ Stack { 260 | => size? * { 261 | size <> -/; 262 | 'stack: []; 263 | 'size: size; 264 | } 265 | 266 | push item * { 267 | ? 'is_full() { 268 | "stack is full (size " + ""?!('size) + ")"!!!; 269 | } 270 | 'stack+: [item]; 271 | } 272 | 273 | push_all items * { 274 | ... element ->? items { 275 | 'push(element); 276 | } 277 | } 278 | 279 | $ * { * 'stack$; } 280 | 281 | pop * { 282 | ? 'is_empty() { 283 | "stack is empty"!!!; 284 | } 285 | out: 'stack<<-/>>; 286 | 'stack-: 'stack$-; 287 | * out; 288 | } 289 | 290 | peek * { 291 | ? 'is_empty() { 292 | "stack is empty"!!!; 293 | } 294 | * 'stack<<-/>>; 295 | } 296 | 297 | ? * { * 'stack$ > \; } 298 | is_full * { * 'size :: 'stack$; } 299 | is_empty * { * 'stack$ :: \; } 300 | 301 | ! * { 302 | ?? { 303 | top: 'peek(); 304 | } !! { 305 | top:; 306 | } 307 | ? top?! :: <-types.String { 308 | top: <-string.wrap(""?!(top), "\""); 309 | } 310 | * "Stack(capacity:" + ""?!('size) 311 | + ", size:" + ""?!('stack$) 312 | + ", top:" + ""?!(top) 313 | + ")"; 314 | } 315 | } -------------------------------------------------------------------------------- /samarium/modules/datetime.sm: -------------------------------------------------------------------------------- 1 | <=types.String; 2 | 3 | MONTHS: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 4 | WEEKDAYS: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; 5 | 6 | days_in_month year month * { 7 | ? month ->? [/, //, /\/, ///, /\\\, /\/\, //\\] { 8 | * /////; 9 | } 10 | ? month ->? [/\, //\, /\\/, /\//] { 11 | * ////\; 12 | } 13 | ? is_leap_year(year) { 14 | * ///\/; 15 | } 16 | * ///\\; 17 | } 18 | 19 | is_leap_year year * { 20 | ? year --- //\\/\\\\ { 21 | * /; 22 | } ,, ? year --- //\\/\\ { 23 | * \; 24 | } ,, ? (year --- /\\) { 25 | * /; 26 | } ,, { 27 | * \; 28 | } 29 | } 30 | 31 | month_name n * { 32 | ? n >: MONTHS$ { 33 | * ""; 34 | } ,, ? n< { 35 | * ""; 36 | } ,, { 37 | * MONTHS<>; 38 | } 39 | } 40 | 41 | sleep_minutes minutes * { 42 | sleep_seconds(minutes ++ ////\\); 43 | } 44 | 45 | sleep_seconds seconds * { 46 | ,., seconds ++ /////\/\\\; 47 | } 48 | 49 | timestamp_utc ts? * { 50 | 60_000: ///\/\/\\//\\\\\; 51 | 3_600_000: //\//\///\///\/\\\\\\\; 52 | ts <> @@; 53 | dtnow: @@@; 54 | tz: dtnow<<-/\..>>; 55 | offset: tz<<\>> ++ 3_600_000 + tz<> ++ 60_000; 56 | * ts - offset; 57 | } 58 | 59 | weekday_name n * { 60 | ? n >: WEEKDAYS$ { 61 | * ""; 62 | } ,, ? n< { 63 | * ""; 64 | } ,, { 65 | * WEEKDAYS<>; 66 | } 67 | } 68 | 69 | @ DateTime { 70 | => Y? M? D? h? m? s? ms? tz? * { 71 | Y <>; 72 | ? :: Y { 73 | 'array: @@@; 74 | 'year: 'array<<\>>; 75 | 'month: 'array<>; 76 | 'day: 'array<>; 77 | 'hour: 'array<>; 78 | 'minute: 'array<>; 79 | 'second: 'array<>; 80 | 'millisecond: 'array<>; 81 | 'timezone: 'array<>; 82 | } ,, { 83 | M <> /; D <> /; 84 | h <> \; m <> \; s <> \; ms <> \; 85 | tz <> [\, \]; 86 | 'year: Y; 87 | 'month: M; 88 | 'day: D; 89 | 'hour: h; 90 | 'minute: m; 91 | 'second: s; 92 | 'millisecond: ms; 93 | 'timezone: tz; 94 | 'array: [Y, M, D, h, m, s, ms, tz<<\>>, tz<>]; 95 | } 96 | ? 'timezone ::: [\, \] { 97 | 'utc: DateTime( 98 | 'year, 'month, 'day, 'hour, 'minute, 'second, 'millisecond 99 | ); 100 | } ,, { 101 | 'utc: '; 102 | } 103 | } 104 | 105 | - other * { 106 | diff: 'to_timestamp() - other.to_timestamp(); 107 | 1000: /\/\ +++ //; 108 | 60: ////\\; 109 | 24: //\\\; 110 | ms: diff --- 1000; diff--: 1000; 111 | s: diff --- 60; diff--: 60; 112 | m: diff --- 60; diff--: 60; 113 | h: diff --- 24; diff--: 24; 114 | == `diff` is days 115 | * DTDiff([diff, h, m, s, ms]); 116 | } 117 | 118 | ! * { 119 | add0s n c * { * <-string.leftpad(String(n), c, "0"); } 120 | * "$Y-$M-$D $h:$m:$s.$z" --- {{ 121 | "Y" -> 'year, 122 | "M" -> add0s('month, /\), 123 | "D" -> add0s('day, /\), 124 | "h" -> add0s('hour + 'timezone<<\>>, /\), 125 | "m" -> add0s('minute + 'timezone<>, /\), 126 | "s" -> add0s('second, /\), 127 | "z" -> add0s('millisecond, //) 128 | }}; 129 | } 130 | 131 | to_timestamp * { 132 | 1000: /\/\ +++ //; 133 | 60: ////\\; 134 | 135 | mod_array: 'array<<..-/\..>>; 136 | array: <-collections.ArithmeticArray(mod_array<<....-/>>); 137 | total: \; 138 | 139 | month_days: days_in_month('year, 'month); 140 | year_days: /\//\//\/ + is_leap_year('year); 141 | 142 | ... i ->? [/, 1000, 60, 60, //\\\, month_days, year_days] { 143 | array++: i; 144 | total+: array<<\>>; 145 | array: array<>; 146 | } 147 | 148 | total-: 'timezone<<\>> ++ 1000 ++ 60 ++ 60; 149 | total-: 'timezone<> ++ 1000 ++ 60; 150 | 151 | * total; 152 | } 153 | } 154 | 155 | @ DTDiff { 156 | => data * { 157 | 'data: data; 158 | } 159 | 160 | ! * { 161 | add0s n c * { * <-string.leftpad(String(n), c, "0"); } 162 | D: 'data<<\>>; 163 | h: 'data<>; 164 | m: 'data<>; 165 | s: 'data<>; 166 | ms: 'data<>; 167 | out: ""; 168 | ? D { out+: ""?!(D) + " days"; } 169 | ? D && (~~ (h || m || s || ms)) { out+: ", "; } 170 | ? h { out+: add0s(h, /\) + ":"; } 171 | ? h || m { out+: add0s(m, /\) + ":"; } 172 | ? h || m || s { out+: add0s(s, /\); } 173 | ? ms { out+: "." + add0s(ms, //); } 174 | * <-string.strip_left(out, "0") || "0"; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /samarium/modules/io.sm: -------------------------------------------------------------------------------- 1 | <=math.to_hex; 2 | <=iter.[any, map]; 3 | <=string.[join, split, strip]; 4 | <=types.[Array, Number, Slice]; 5 | 6 | 7 | @ Bytes { 8 | => value * { 9 | '#value: '#to_bytes(value); 10 | } 11 | 12 | #to_bytes v * { 13 | ? v?! :: ""?! { 14 | * [i% ... i ->? v]; 15 | } 16 | ? v?! :: []?! { 17 | ? any([i?! ::: /?! ... i ->? v]) { 18 | "non-int found in the list"!!!; 19 | } 20 | * v; 21 | } ,, { 22 | "invalid type: " + ""?!(v?!)!!!; 23 | } 24 | } 25 | 26 | + other * { 27 | ? other?! :: Bytes { 28 | * Bytes('#value + other.export()); 29 | } 30 | * Bytes('#value + '#to_bytes(other)); 31 | } 32 | 33 | ! * { 34 | * join([to_hex(i) ... i ->? '#value], " "); 35 | } 36 | 37 | export * { * '#value; } 38 | 39 | export_string * { 40 | * join([i% ... i ->? '#value]); 41 | } 42 | } 43 | 44 | inputcast prompt? * { 45 | prompt <> ""; 46 | * #autocast(prompt???); 47 | } 48 | 49 | #autocast value * { 50 | value: strip(value); 51 | ? ~~ value { *; } 52 | ? ".." ->? value { 53 | bits: [Integer(i) ? i ,, ... i ->? split(value, "..")]; 54 | ? bits$ > // { 55 | "too many arguments for slice"!!!; 56 | } 57 | * Slice(**bits); 58 | } 59 | ? "=" ->? value { 60 | items: [split(i, "=") ... i ->? split(value, ",")]; 61 | * {{strip(k) -> #autocast(v) ... k, v ->? items}}; 62 | } 63 | ... sep ->? ", " { 64 | ? sep ->? value { 65 | * Array(map(#autocast, split(value, sep))); 66 | } 67 | } 68 | ?? { * Integer(value); } 69 | !! { * value; } 70 | } 71 | 72 | read_until target? * { 73 | target <> ""; 74 | s: ""; 75 | .. { 76 | i: ???; 77 | s+: i; 78 | ? i :: target { <- } 79 | s+: "\n"; 80 | } 81 | * s; 82 | } -------------------------------------------------------------------------------- /samarium/modules/iter.sm: -------------------------------------------------------------------------------- 1 | accumulate array function * { 2 | prev: array<<\>>; 3 | ** prev; 4 | ... e ->? array<> { 5 | prev: function(prev, e); 6 | ** prev; 7 | } 8 | } 9 | 10 | all array * { 11 | ... i ->? array { 12 | ? ~~ i { 13 | * \; 14 | } 15 | } 16 | * /; 17 | } 18 | 19 | any array * { 20 | ... i ->? array { 21 | ? i { 22 | * /; 23 | } 24 | } 25 | * \; 26 | } 27 | 28 | chunks array size * { 29 | ... i ->? <<..array$..size>> { 30 | ** array<>; 31 | } 32 | } 33 | 34 | count array target * { 35 | ? target ~~ ->? array { * \; } 36 | s: \; 37 | ... e ->? array { 38 | s+: e :: target; 39 | } 40 | * s; 41 | } 42 | 43 | cycle iter * { 44 | saved: [e ... e ->? iter]; 45 | .. { 46 | ... e ->? saved { 47 | ** e; 48 | } 49 | } 50 | } 51 | 52 | drop_while array function * { 53 | i: \; 54 | ... e ->? array { 55 | i+:; 56 | ? ~~ function(e) { 57 | ** e; 58 | <- 59 | } 60 | } 61 | ... e ->? array<> { 62 | ** e; 63 | } 64 | } 65 | 66 | filter function array * { 67 | ? function$ :: / { 68 | array: [[**e] ... e ->? array]; 69 | } 70 | ... e ->? array { 71 | ? function(**e) { 72 | ** e; 73 | } 74 | } 75 | } 76 | 77 | filter_false function array * { 78 | flip arg * { * ~~ function(arg); } 79 | * filter(flip, array); 80 | } 81 | 82 | find array target * { 83 | index: \; 84 | ? array?! :: <-types.String { 85 | .. index + target$ <: array$ { 86 | ? array<> :: target { 87 | * index; 88 | } 89 | index+:; 90 | } 91 | } ,, { 92 | ... e ->? array { 93 | ? e :: target { 94 | * index; 95 | } 96 | index+:; 97 | } 98 | } 99 | * -/; 100 | } 101 | 102 | 103 | find_all array target * { 104 | ? array?! :: <-types.String && target$ > / { 105 | ... i ->? <<..array$>> { 106 | substring: array<>; 107 | ? substring :: target { 108 | ** i; 109 | } 110 | } 111 | } ,, { 112 | ... i, v ->? <<>> >< array { 113 | ? v :: target { 114 | ** i; 115 | } 116 | } 117 | } 118 | } 119 | 120 | flatten array depth? * { 121 | depth <>; 122 | ? depth ::: && depth< { 123 | "depth cannot be negative"!!!; 124 | } 125 | x: [i ... i ->? array]; 126 | 127 | ? depth :: \ { 128 | * x; 129 | } 130 | 131 | ? depth :: { 132 | old: x; 133 | new: #flatten(x); 134 | .. new ::: old { 135 | old: [i ... i ->? new]; 136 | new: #flatten(old); 137 | } 138 | * new; 139 | } ,, { 140 | flattened: x; 141 | ... _ ->? <<..depth>> { 142 | flattened: #flatten(flattened); 143 | } 144 | * flattened; 145 | } 146 | } 147 | 148 | #flatten array * { 149 | flattened: []; 150 | ... i ->? array { 151 | ? i?! :: []?! { 152 | flattened+: i; 153 | } ,, { 154 | flattened+: [i]; 155 | } 156 | } 157 | * flattened; 158 | } 159 | 160 | map function array * { 161 | ? function$ :: / { 162 | array: [[e] ... e ->? array]; 163 | } 164 | ... e ->? array { 165 | ** function(**e); 166 | } 167 | } 168 | 169 | pairwise array * { 170 | ... i ->? <<..array$ - />> { 171 | ** array<>; 172 | } 173 | } 174 | 175 | reduce function array * { 176 | prev: array<<\>>; 177 | ... e ->? array<> { 178 | prev: function(prev, e); 179 | } 180 | * prev; 181 | } 182 | 183 | reverse array * { 184 | i: array$-; 185 | .. i>: { 186 | ** array<>; 187 | i-:; 188 | } 189 | } 190 | 191 | sorted array key? * { 192 | key <> #dummy; 193 | decorated: [[key(v), v] ... v ->? array]; 194 | d_sorted: #quicksort(decorated); 195 | * [v ... _, v ->? d_sorted]; 196 | } 197 | 198 | #dummy x * { * x; } 199 | 200 | #quicksort array * { 201 | x: [i ... i ->? array]; 202 | ? x$ < /\ { * x; } 203 | pivot: x<<\>>; 204 | less: [i ... i ->? x<> ? i <: pivot]; 205 | greater: [i ... i ->? x<> ? i > pivot]; 206 | * #quicksort(less) + [pivot] + #quicksort(greater); 207 | } 208 | 209 | take_while array function * { 210 | ... e ->? array { 211 | ? function(e) { 212 | ** e; 213 | -> 214 | } 215 | <- 216 | } 217 | } 218 | 219 | zip_longest fill arrays... * { 220 | longest: <-math.max([i$ ... i ->? arrays]); 221 | ... i ->? <<..longest>> { 222 | ... it ->? arrays { 223 | ?? { 224 | ** it<>; 225 | } !! { 226 | ** fill; 227 | } 228 | } 229 | } 230 | } -------------------------------------------------------------------------------- /samarium/modules/math.sm: -------------------------------------------------------------------------------- 1 | E: /\`/\//\//////\\\\/\/\/\\\/\//\\\/\/\\\/\/\///\//\/\\/\/\/\\//\/\/\/\//////\///\\\/\/\//\\\/\\\\\\\/\\/; 2 | PHI: /`/\\////\\\//\///\////\\//\///\\/\///////\/\\/\/\\/////\\\\\/\/\/////\\///\\///\\//\\\\\\\//\\\\\\/\/; 3 | PI: //`\\/\\/\\\\//////\//\/\/\/\\\/\\\/\\\\/\//\/\\\//\\\\/\\\//\/\\//\\\/\\//\\\//\\//\\\/\/\\\/\///\\\\\; 4 | TAU: PI++; 5 | 6 | 7 | abs x * { 8 | ? x < \ { * -x; } 9 | * x; 10 | } 11 | 12 | ceil x * { 13 | ? is_int(x) { * x; } 14 | * x$+; 15 | } 16 | 17 | factorial n * { 18 | ? n < \ { 19 | "factorial is defined only for non-negative numbers"!!!; 20 | } 21 | ? ~~is_int(n) { 22 | "n has to be an integer"!!!; 23 | } 24 | o: /; 25 | .. n > \ { 26 | o++: n; 27 | n-: /; 28 | } 29 | * o; 30 | } 31 | 32 | floor x * { 33 | * x$; 34 | } 35 | 36 | gcd a b * { 37 | .. b { a, b: b, a --- b; } 38 | * abs(a); 39 | } 40 | 41 | is_int x * { 42 | * x :: x$; 43 | } 44 | 45 | lcm a b * { 46 | ?? { * abs(a ++ b) -- gcd(a, b); } 47 | !! { * \; } 48 | } 49 | 50 | max array b... * { 51 | ? b { * max([array] + b); } 52 | o: array<<\>>; 53 | ... e ->? array { 54 | ? e > o { o: e; } 55 | } 56 | * o; 57 | } 58 | 59 | min array b... * { 60 | ? b { * min([array] + b); } 61 | o: array<<\>>; 62 | ... e ->? array { 63 | ? e < o { o: e; } 64 | } 65 | * o; 66 | } 67 | 68 | product array * { 69 | o: /; 70 | ... e ->? array { o++: e; } 71 | * o; 72 | } 73 | 74 | shl a b * { 75 | * a ++ /\ +++ b; 76 | } 77 | 78 | shr a b * { 79 | * a -- /\ +++ b; 80 | } 81 | 82 | sqrt x * { 83 | ? x < \ { 84 | "sqrt is defined only for non-negative numbers"!!!; 85 | } 86 | * x +++ `/; 87 | } 88 | 89 | sum array start? * { 90 | start <> \; 91 | o: start; 92 | ... e ->? array { o+: e; } 93 | * o; 94 | } 95 | 96 | #to_base n base base_name charset * { 97 | ? ~~ is_int(n) { 98 | "cannot convert a non-integer to $0" --- base_name!!!; 99 | } 100 | div: (n -- base)$; 101 | mod: n --- base; 102 | ? ~~ div { * charset<>; } 103 | * #to_base(div, base, base_name, charset) + charset<>; 104 | } 105 | 106 | to_bin n * { 107 | * #to_base(n, /\, "bin", "01"); 108 | } 109 | 110 | to_hex n * { 111 | * #to_base(n, /\\\\, "hex", <-string.HEXDIGITS); 112 | } 113 | 114 | to_oct n * { 115 | * #to_base(n, /\\\, "oct", <-string.OCTDIGITS); 116 | } 117 | 118 | is_prime n * { 119 | ? ~~is_int(n) { * \; } 120 | ? n <: // { * n > /; } 121 | ? n --- :: \ || n --- // :: \ { * \; } 122 | i: /\/; 123 | .. i ++ i <: n { 124 | ? n --- i :: \ || n --- (i + /\) :: \ { * \; } 125 | i+: //\; 126 | } 127 | * /; 128 | } 129 | 130 | #r: <-pystd.round_; 131 | 132 | round n ndigits? * { 133 | ndigits <>; 134 | * #r(n, ndigits); 135 | } -------------------------------------------------------------------------------- /samarium/modules/operator.sm: -------------------------------------------------------------------------------- 1 | add x y * { 2 | * x + y; 3 | } 4 | 5 | and x y * { 6 | * x & y; 7 | } 8 | 9 | cast x * { 10 | * x%; 11 | } 12 | 13 | div x y * { 14 | * x -- y; 15 | } 16 | 17 | eq x y * { 18 | * x :: y; 19 | } 20 | 21 | ge x y * { 22 | * x >: y; 23 | } 24 | 25 | gt x y * { 26 | * x > y; 27 | } 28 | 29 | has x y * { 30 | * y ->? x; 31 | } 32 | 33 | hash x * { 34 | * x##; 35 | } 36 | 37 | le x y * { 38 | * x <: y; 39 | } 40 | 41 | lt x y * { 42 | * x < y; 43 | } 44 | 45 | mod x y * { 46 | * x --- y; 47 | } 48 | 49 | mul x y * { 50 | * x ++ y; 51 | } 52 | 53 | not x * { 54 | * ~x; 55 | } 56 | 57 | ne x y * { 58 | * x ::: y; 59 | } 60 | 61 | or x y * { 62 | * x | y; 63 | } 64 | 65 | pow x y * { 66 | * x +++ y; 67 | } 68 | 69 | random x * { 70 | * x??; 71 | } 72 | 73 | special x * { 74 | * x$; 75 | } 76 | 77 | sub x y * { 78 | * x - y; 79 | } 80 | 81 | to_bit x * { 82 | * / ? x ,, \; 83 | } 84 | 85 | to_string x * { 86 | * ""?!(x); 87 | } 88 | 89 | xor x y * { 90 | * x ^ y; 91 | } -------------------------------------------------------------------------------- /samarium/modules/pystd.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: INP001 2 | from __future__ import annotations 3 | 4 | from samarium.python import export 5 | 6 | 7 | @export 8 | def round_(x: float, ndigits: int | None = None) -> float: 9 | return round(x, ndigits) 10 | -------------------------------------------------------------------------------- /samarium/modules/random.sm: -------------------------------------------------------------------------------- 1 | choices array k * { 2 | * [array?? ... _ ->? <<..k>>]; 3 | } 4 | 5 | randint start end * { 6 | * (end - start+)?? + start; 7 | } 8 | 9 | sample array k * { 10 | ? k > array$ { 11 | "the sample cannot be bigger than the array"!!!; 12 | } 13 | o: []; 14 | ? array?! :: <-types.Slice { 15 | old_array: []?!(array); 16 | } ,, { 17 | old_array: array<<>>; 18 | } 19 | .. o$ < k { 20 | e: old_array??; 21 | o+: [e]; 22 | old_array-: [e]; 23 | } 24 | * o; 25 | } 26 | 27 | shuffle array * { 28 | ? array?! :: <-types.Slice { 29 | "cannot shuffle slices"!!!; 30 | } 31 | shuffled_array: []; 32 | old_array: array<<>>; 33 | .. old_array$> { 34 | e: old_array??; 35 | shuffled_array+: [e]; 36 | old_array-: [e]; 37 | } 38 | * shuffled_array; 39 | } -------------------------------------------------------------------------------- /samarium/modules/string.sm: -------------------------------------------------------------------------------- 1 | UPPERCASE: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 2 | LOWERCASE: "abcdefghijklmnopqrstuvwxyz"; 3 | LETTERS: UPPERCASE + LOWERCASE; 4 | 5 | OCTDIGITS: "01234567"; 6 | DIGITS: OCTDIGITS + "89"; 7 | HEXDIGITS: DIGITS + "abcdef"; 8 | 9 | PUNCTUATION: "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; 10 | WHITESPACE: " \t\n\r\f\v"; 11 | 12 | PRINTABLE: LETTERS + DIGITS + PUNCTUATION + WHITESPACE; 13 | 14 | capitalize string * { 15 | ? ~~ string { * string; } 16 | * to_upper(string<<\>>) + to_lower(string<>); 17 | } 18 | 19 | center string length char? * { 20 | char <> " "; 21 | ? char$ > / { 22 | "char has to be of length 1"!!!; 23 | } 24 | index: \; 25 | .. string$ < length { 26 | ? index { 27 | string: char + string; 28 | } ,, { 29 | string+: char; 30 | } 31 | index+:; 32 | index---:; 33 | } 34 | * string; 35 | } 36 | 37 | ends_with string suffix * { 38 | ? suffix$ > string$ { * \; } 39 | ? suffix :: string { * /; } 40 | * string<<-suffix$..>> :: suffix; 41 | } 42 | 43 | is_alphabetic string * { 44 | * is_in_group(string, LETTERS); 45 | } 46 | 47 | is_alphanumeric string * { 48 | * is_in_group(string, LETTERS + DIGITS); 49 | } 50 | 51 | is_capitalized string * { 52 | * string :: capitalize(string); 53 | } 54 | 55 | is_decimal string * { 56 | * is_in_group(string, DIGITS); 57 | } 58 | 59 | is_hexadecimal string * { 60 | * is_in_group(string, HEXDIGITS); 61 | } 62 | 63 | is_in_group string group * { 64 | ... char ->? string { 65 | ? char ~~ ->? group { * \; } 66 | } 67 | * /; 68 | } 69 | 70 | is_lower string * { 71 | * string :: to_lower(string); 72 | } 73 | 74 | is_octal string * { 75 | * is_in_group(string, OCTDIGITS); 76 | } 77 | 78 | is_title string * { 79 | * string :: title(string); 80 | } 81 | 82 | is_upper string * { 83 | * string :: to_upper(string); 84 | } 85 | 86 | is_wrapped string chars * { 87 | * starts_with(string, chars) && ends_with(string, chars); 88 | } 89 | 90 | join iterable delimiter? * { 91 | delimiter <> ""; 92 | o: ""; 93 | ... e ->? iterable { 94 | o+: ""?!(e); 95 | o+: delimiter; 96 | } 97 | ? delimiter { 98 | o: o<<..-delimiter$>>; 99 | } 100 | * o; 101 | } 102 | 103 | leftpad string length char? * { 104 | char <> " "; 105 | * pad(string, length, char) + string; 106 | } 107 | 108 | ordinal n * { 109 | 10: /\/\; 110 | ords: {{/ -> "st", /\ -> "nd", // -> "rd"}}; 111 | ? n --- (10 ++ 10) ~~ ->? <> && n --- 10 ->? ords { 112 | * "$0$1" --- [n, ords<>]; 113 | } 114 | * "$0th" --- [n]; 115 | } 116 | 117 | pad string length char? * { 118 | char <> " "; 119 | ? char$ > / { 120 | "char has to be of length 1"!!!; 121 | } 122 | ? length < string$ { * string; } 123 | * char ++ (length - string$); 124 | } 125 | 126 | replace string replacements count? * { 127 | count <> -/; 128 | 129 | replace string from to count * { 130 | empty * { 131 | ? ~~ from { * to; } 132 | * string; 133 | } 134 | 135 | interleave count * { 136 | out: to; 137 | count-:; 138 | ... i ->? string { 139 | out+: i; 140 | ? count { 141 | out+: to; 142 | count-:; 143 | } 144 | } 145 | * out; 146 | } 147 | 148 | del_char count * { 149 | out: ""; 150 | ... i ->? string { 151 | ? count && i :: from { 152 | count-:; 153 | } ,, { 154 | out+: i; 155 | } 156 | } 157 | * out; 158 | } 159 | 160 | replace_substring count * { 161 | string_copy: string; 162 | out: ""; 163 | find: <-iter.find; 164 | .. count && from ->? string_copy { 165 | i: find(string_copy, from); 166 | out+: string_copy<<..i>>; 167 | out+: to; 168 | string_copy: string_copy<>; 169 | count-:; 170 | } 171 | * out + string_copy; 172 | } 173 | 174 | replace_char count * { 175 | out: ""; 176 | ... i ->? string { 177 | ? i :: from && count { 178 | out+: to; 179 | count-:; 180 | } ,, { 181 | out+: i; 182 | } 183 | } 184 | * out; 185 | } 186 | 187 | ? ~~ string { 188 | * empty(); 189 | } ,, ? ~~ from { 190 | * interleave(count); 191 | } ,, ? from$ :: / { 192 | ? ~~ to { 193 | * del_char(count); 194 | } 195 | * replace_char(count); 196 | } ,, { 197 | * replace_substring(count); 198 | } 199 | } 200 | 201 | ? replacements?! ::: <-types.Table { 202 | "invalid type for replacements: " + ""?!(replacements?!)!!!; 203 | } 204 | replacements: {{k -> v ... k, v ->? replacements >< replacements$}}; 205 | ? ~~ <-iter.all([count, replacements]) { 206 | * string; 207 | } 208 | ... f, t ->? replacements >< replacements$ { 209 | string: replace(string, f, t, count); 210 | } 211 | * string; 212 | } 213 | 214 | rightpad string length char? * { 215 | char <> " "; 216 | * string + pad(string, length, char); 217 | } 218 | 219 | split string separator? * { 220 | separator <> " "; 221 | ? ~~ separator { ["empty separator", "use []?!(string) instead"]!!!; } 222 | ? separator?! :: []?! { 223 | first: separator<<\>>; 224 | rest: separator<>; 225 | string: replace(string, {{i -> first ... i ->? rest}}); 226 | separator: first; 227 | } 228 | ? separator ~~ ->? string { * [string]; } 229 | ? separator$ :: / { 230 | * #split_char(string, separator); 231 | } 232 | out: []; 233 | <=iter.find; 234 | .. { 235 | idx: find(string, separator); 236 | ? idx :: -/ { <- } 237 | out+: [string<<..idx>>]; 238 | string: string<>; 239 | } 240 | * out + [string]; 241 | } 242 | 243 | #split_char string separator * { 244 | out: []; 245 | temp: ""; 246 | ... char ->? string { 247 | ? char :: separator { 248 | out+: [temp]; 249 | temp: ""; 250 | } ,, { 251 | temp+: char; 252 | } 253 | } 254 | * out + [temp]; 255 | } 256 | 257 | split_lines string * { 258 | * split( 259 | replace(string, {{"\r\n" -> "\n"}}), 260 | []?!("\r\n\v\f\x1c\x1d\x1e\x85\u2028\u2029") 261 | ); 262 | } 263 | 264 | starts_with string prefix * { 265 | ? prefix$ > string$ { * \; } 266 | ? prefix :: string { * /; } 267 | * string<<..prefix$>> :: prefix; 268 | } 269 | 270 | strip string chars? * { 271 | chars <> " "; 272 | * strip_left(strip_right(string, chars), chars); 273 | } 274 | 275 | strip_left string prefix * { 276 | .. starts_with(string, prefix) { 277 | string: string<>; 278 | } 279 | * string; 280 | } 281 | 282 | strip_right string suffix * { 283 | .. ends_with(string, suffix) { 284 | string: string<<..-suffix$>>; 285 | } 286 | * string; 287 | } 288 | 289 | swapcase string * { 290 | out: ""; 291 | ... char ->? string { 292 | ? is_upper(char) { 293 | out+: to_lower(char); 294 | } ,, { 295 | out+: to_upper(char); 296 | } 297 | } 298 | * out; 299 | } 300 | 301 | title string * { 302 | * join([capitalize(w) ... w ->? split(string)], " "); 303 | } 304 | 305 | to_lower string * { 306 | ? string?! ::: <-types.String { 307 | "invalid type: " + ""?!(string?!)!!!; 308 | } 309 | out: ""; 310 | ... char ->? string { 311 | ? char% ->? <> { 312 | out+: (char% + /\\\\\)%; 313 | } ,, { 314 | out+: char; 315 | } 316 | } 317 | * out; 318 | } 319 | 320 | to_upper string * { 321 | ? string?! ::: <-types.String { 322 | "invalid type: " + ""?!(string?!)!!!; 323 | } 324 | out: ""; 325 | ... char ->? string { 326 | ? char% ->? <> { 327 | out+: (char% - /\\\\\)%; 328 | } ,, { 329 | out+: char; 330 | } 331 | } 332 | * out; 333 | } 334 | 335 | wrap string wrapper * { 336 | * wrapper + string + wrapper; 337 | } -------------------------------------------------------------------------------- /samarium/modules/types.sm: -------------------------------------------------------------------------------- 1 | Array: []?!; 2 | Number: \?!; 3 | Null: (||)?!; 4 | Slice: <<>>?!; 5 | String: ""?!; 6 | Table: {{}}?!; 7 | Zip: ("" >< "")?!; 8 | 9 | 10 | @ Boolean { 11 | => value * { 12 | ? value?! :: ""?! { 13 | 'value: {{"true" -> /, "false" -> \}}<<<-string.to_lower(value)>>; 14 | } ,, { 15 | 'value: / ? value ,, \; 16 | } 17 | } 18 | 19 | ! * { * "true" ? 'value ,, "false"; } 20 | ? * { * 'value; } 21 | 22 | val obj * { 23 | ? obj?! :: '?! { * obj.value; } 24 | * obj; 25 | } 26 | 27 | :: other * { 28 | * Boolean('value :: 'val(other)); 29 | } 30 | 31 | > other * { * Boolean('value > 'val(other)); } 32 | 33 | 34 | + other * { 35 | out: 'value + 'val(other); 36 | ? out > / { 37 | * out; 38 | } ,, { 39 | * Boolean(out); 40 | } 41 | } 42 | 43 | - other * { 44 | out: 'value - 'val(other); 45 | ? out< { 46 | * out; 47 | } ,, { 48 | * Boolean(out); 49 | } 50 | } 51 | 52 | ++ other * { 53 | * Boolean('value ++ 'val(other)); 54 | } 55 | 56 | -- other * { 57 | * Boolean('value -- 'val(other)); 58 | } 59 | 60 | +++ other * { 61 | * Boolean('value +++ 'val(other)); 62 | } 63 | 64 | --- other * { 65 | * Boolean('value --- 'val(other)); 66 | } 67 | 68 | & other * { * Boolean('value & 'val(other)); } 69 | ^ other * { * Boolean('value ^ 'val(other)); } 70 | | other * { * Boolean('value | 'val(other)); } 71 | 72 | ~ * { * Boolean(\ ? 'value ,, /); } 73 | } 74 | 75 | @ UUID4 { 76 | => * { 77 | hex: <-string.join(<-random.choices(<-string.HEXDIGITS, /\\\\\)); 78 | hex<>: "4"; 79 | hex<>: "89ab"??; 80 | 'hex: hex; 81 | 'dec: Integer("x:" + hex); 82 | } 83 | 84 | ! * { 85 | h: 'hex; 86 | 8: /\\\; 87 | 12: //\\; 88 | 16: /\\\\; 89 | 20: /\/\\; 90 | * <-string.join( 91 | [h<<..8>>, h<<8..12>>, h<<12..16>>, h<<16..20>>, h<<20..>>], 92 | "-" 93 | ); 94 | } 95 | } 96 | 97 | @ Frozen { 98 | => val * { 99 | t: val?!; 100 | ? t ~~ ->? [Array, Table] { 101 | "type $0 is already frozen" --- [t]!!!; 102 | } 103 | ? t :: Array { 104 | '#val: val<<>>; 105 | } ,, { 106 | '#val: {{k -> v ... k, v ->? val >< val$}}; 107 | } 108 | '#t: t; 109 | } 110 | 111 | ## * { * <-iter.reduce(<-operator.xor, [//\/\\/\\\/\\\\/\\\\\/] + '#val); } 112 | ! * { * "Frozen($0)" --- ""?!('#val); } 113 | ->? element * { * element ->? '#val; } 114 | ... * { ... i ->? '#val { ** i; } } 115 | <<>> index * { * '#val<>; } 116 | >< other * { * '#val >< other; } 117 | :: other * { * '#val :: other.#val; } 118 | ? * { * / ? '#val ,, \; } 119 | $ * { * '#val$; } 120 | } 121 | -------------------------------------------------------------------------------- /samarium/python.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from collections.abc import Iterable as PyIterable 5 | from enum import Enum as PyEnum 6 | from io import BufferedIOBase, BufferedReader, BufferedWriter, TextIOWrapper 7 | from types import FunctionType 8 | 9 | from samarium.classes import ( 10 | NULL, 11 | Array, 12 | Attrs, 13 | Enum, 14 | File, 15 | Function, 16 | Iterator, 17 | Mode, 18 | Null, 19 | Num, 20 | Number, 21 | Slice, 22 | String, 23 | Table, 24 | Zip, 25 | ) 26 | from samarium.utils import get_type_name 27 | 28 | 29 | class SliceRange: 30 | def __init__(self, slice_: Slice) -> None: 31 | self._slice = slice_ 32 | 33 | @property 34 | def slice(self) -> slice: 35 | return self._slice.val 36 | 37 | @property 38 | def range(self) -> range: 39 | return self._slice.range 40 | 41 | 42 | def to_python(obj: object) -> object: 43 | if isinstance(obj, (String, Number, Zip, File)): 44 | return obj.val 45 | if isinstance(obj, Null): 46 | return None 47 | if isinstance(obj, Array): 48 | return [to_python(i) for i in obj.val] 49 | if isinstance(obj, Table): 50 | return {to_python(k): to_python(v) for k, v in obj.val.items()} 51 | if isinstance(obj, Slice): 52 | return SliceRange(obj) 53 | if isinstance(obj, Enum): 54 | o = {k.removeprefix("sm_"): to_python(v) for k, v in obj.members.items()} 55 | return PyEnum(obj.name, o) 56 | if isinstance(obj, Iterator): 57 | return map(to_python, obj) 58 | msg = f"Conversion for type {type(obj).__name__!r} not found" 59 | raise TypeError(msg) 60 | 61 | 62 | def to_samarium(obj: object) -> Attrs: 63 | if isinstance(obj, (int, bool, float)): 64 | return Num(obj) 65 | if isinstance(obj, str): 66 | return String(obj) 67 | if obj is None: 68 | return NULL 69 | if isinstance(obj, FunctionType): 70 | return Function(obj) 71 | if isinstance(obj, (list, tuple, set)): 72 | return Array([to_samarium(i) for i in obj]) 73 | if isinstance(obj, dict): 74 | return Table({to_samarium(k): to_samarium(v) for k, v in obj.items()}) 75 | if isinstance(obj, (range, slice)): 76 | return Slice( 77 | to_samarium(None if obj.start == 0 else obj.start), 78 | to_samarium(obj.stop), 79 | to_samarium(None if obj.step == 1 else obj.step), 80 | ) 81 | if isinstance(obj, SliceRange): 82 | return obj._slice 83 | if isinstance(obj, (TextIOWrapper, BufferedWriter, BufferedReader)): 84 | return File( 85 | obj, Mode(obj.mode).name, obj.name, binary=isinstance(obj, BufferedIOBase) 86 | ) 87 | if isinstance(obj, zip): 88 | return Zip(*obj) 89 | if isinstance(obj, type) and issubclass(obj, PyEnum): 90 | o = {f"sm_{k}": to_samarium(v) for k, v in obj.__members__.items()} 91 | return Enum(f"sm_{obj.__name__}", **o) 92 | if isinstance(obj, PyEnum): 93 | return to_samarium(obj.value) 94 | if isinstance(obj, PyIterable): 95 | return Iterator(obj) 96 | msg = f"Conversion for type {type(obj).__name__!r} not found" 97 | raise TypeError(msg) 98 | 99 | 100 | def export(func: Callable) -> Callable[..., Attrs]: 101 | """Wraps a Python function to be used in Samarium""" 102 | 103 | if not isinstance(func, FunctionType): 104 | msg = f"cannot export a non-function type {get_type_name(func)!r}" 105 | raise TypeError(msg) 106 | 107 | def wrapper(*_args: Attrs) -> Attrs: 108 | return to_samarium(func(*map(to_python, _args))) 109 | 110 | f = Function(wrapper) 111 | f.__pyexported__ = True 112 | 113 | return f 114 | -------------------------------------------------------------------------------- /samarium/repl.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import string 5 | import sys 6 | from ast import literal_eval 7 | from contextlib import redirect_stderr, redirect_stdout, suppress 8 | from dataclasses import dataclass 9 | from datetime import datetime, timedelta 10 | from os import get_terminal_size 11 | from pathlib import Path 12 | from time import time 13 | from typing import TYPE_CHECKING, TypedDict 14 | 15 | if TYPE_CHECKING: 16 | from collections.abc import Iterator 17 | 18 | if sys.platform not in ("win32", "cygwin"): 19 | # used by input() to provide elaborate line editing & history features 20 | import readline # noqa: F401 21 | 22 | from crossandra import CrossandraError 23 | 24 | from samarium import core 25 | from samarium.exceptions import DAHLIA, SamariumSyntaxError, handle_exception 26 | from samarium.runtime import Runtime 27 | from samarium.tokenizer import tokenize 28 | from samarium.transpiler import Registry, match_brackets 29 | from samarium.utils import __version__ 30 | 31 | TIME = {"t", "time"} 32 | HELP = {"?", "h", "help"} 33 | QUIT = {"!", "exit", "q", "quit", "exit!", "q!", "quit!"} 34 | COMMANDS = ( 35 | { 36 | "3", 37 | "clear", 38 | "color", 39 | "debug", 40 | "restore", 41 | "run", 42 | "session", 43 | "undo", 44 | } 45 | | HELP 46 | | TIME 47 | | QUIT 48 | ) 49 | NO_ARG_SESSION_SUBCMDS = {"delete-all", "list", "restore"} 50 | 51 | COLOR_TO_CODE = { 52 | "blue": "&1", 53 | "green": "&2", 54 | "cyan": "&3", 55 | "red": "&4", 56 | "purple": "&5", 57 | "orange": "&6", 58 | "light gray": "&7", 59 | "gray": "&8", 60 | "light blue": "&9", 61 | "black": "&0", 62 | "lime": "&a", 63 | "turquoise": "&b", 64 | "light red": "&c", 65 | "pink": "&d", 66 | "yellow": "&e", 67 | "default": "", 68 | } 69 | CODE_TO_COLOR = {v: k for k, v in COLOR_TO_CODE.items()} 70 | 71 | CACHE_DIR = Path.home() / ".cache" / "samarium" 72 | CONFIG_FILE = Path.home() / ".config" / "samarium" / "repl.yaml" 73 | PS1 = "--> " 74 | PS2 = " > " 75 | 76 | 77 | class Command: 78 | def __init__( 79 | self, *aliases: str, arg: str = "", sep: str = "|", msg: str = "" 80 | ) -> None: 81 | self.aliases = aliases 82 | self.arg = arg 83 | self.sep = sep 84 | self.msg = msg 85 | 86 | def __str__(self) -> str: 87 | return ( 88 | self.sep.join(f"&e{a}&R" for a in self.aliases) + f" &7{self.arg}&R" 89 | ).ljust(28 + 4 * len(self.aliases)).lstrip() + self.msg 90 | 91 | 92 | HELP_TEXT = "\n".join( 93 | map( 94 | str, 95 | ( 96 | Command("?", "h", "help", msg="shows this message"), 97 | Command("exit", "q", "quit", msg="saves the session and quits the repl"), 98 | Command("!", "exit!", "q!", "quit!", msg="force quits the repl"), 99 | Command("session", msg="manages sessions, see &2:? session&R for details"), 100 | Command("clear", msg="clears the screen"), 101 | Command( 102 | "color", msg="changes the prompt color, see &2:? color&R for details" 103 | ), 104 | Command("debug", msg="toggles debug mode"), 105 | Command("restore", msg="restores the previous session"), 106 | Command("t", "time", msg="times the execution of the following statement"), 107 | Command("undo", msg="undoes the last statement"), 108 | ), 109 | ) 110 | ) 111 | 112 | COLOR_HELP_TEXT = """ 113 | &ecolor &7[color]&R 114 | &oproviding no color will reset it to the default one&R 115 | 116 | use &e:color save&R to save the current color to your config 117 | 118 | &00&R|&0black&R 119 | &11&R|&1blue&R 120 | &22&R|&2green&R 121 | &33&R|&3cyan&R 122 | &44&R|&4red&R 123 | &55&R|&5purple&R 124 | &66&R|&6orange&R 125 | &77&R|&7light gray&R 126 | &88&R|&8gray&R 127 | &99&R|&9light blue&R 128 | &aa&R|&alime&R 129 | &bb&R|&bturquoise&R 130 | &cc&R|&clight red&R 131 | &dd&R|&dpink&R 132 | &ee&R|&eyellow&R 133 | """ 134 | 135 | SESSIONS_HELP_TEXT = "\n".join( 136 | map( 137 | str, 138 | ( 139 | Command("session autosave", msg="toggles session autosave on exit"), 140 | Command("session delete-all", msg="deletes all saved sessions"), 141 | Command( 142 | "session lifetime", 143 | arg="[time]", 144 | msg=( 145 | "updates session lifetime (how long they stay cached)\n" 146 | f"{' ' * 24}&ocurrent value is shown when run without an argument&R" 147 | ), 148 | ), 149 | Command("session list", msg="lists all sessions and their sizes"), 150 | Command("session load", arg="", msg="loads a session"), 151 | Command("session restore", msg="restores the last unnamed session"), 152 | Command("session save", arg="[name]", msg="saves a session"), 153 | ), 154 | ) 155 | ) 156 | 157 | HELP_TEXT_INDEX = { 158 | "": HELP_TEXT, 159 | "session": SESSIONS_HELP_TEXT, 160 | "sessions": SESSIONS_HELP_TEXT, 161 | "color": COLOR_HELP_TEXT, 162 | } 163 | 164 | 165 | @dataclass 166 | class REPLConfig: 167 | color: str = "" 168 | autosave: bool = True 169 | session_lifetime: float = 30.0 170 | 171 | @classmethod 172 | def load(cls) -> REPLConfig: 173 | lines = CONFIG_FILE.read_text().splitlines() 174 | data = { 175 | k.strip(): literal_eval(v.strip()) 176 | for k, _, v in (line.partition(":") for line in lines) 177 | } 178 | return cls(**data) 179 | 180 | def save(self) -> None: 181 | content = "\n".join(f"{k}: {v!r}" for k, v in vars(self).items()) 182 | CONFIG_FILE.write_text(content) 183 | 184 | 185 | class SessionData(TypedDict): 186 | color: str 187 | debug: bool 188 | history: list[str] 189 | 190 | 191 | class Session: 192 | ALLOWED_CHARS = string.ascii_letters + string.digits + "-_" 193 | 194 | def __init__( 195 | self, color: str, *, debug: bool, history: list[str] | None = None 196 | ) -> None: 197 | self.color = color 198 | self.debug = debug 199 | self.history = history or [] 200 | 201 | @property 202 | def color(self) -> str: 203 | return self._color 204 | 205 | @color.setter 206 | def color(self, value: str) -> None: 207 | if not value: 208 | self._color = "" 209 | elif (code := value.removeprefix("&")) in "0123456789abcde": 210 | self._color = f"&{code}" 211 | elif value in COLOR_TO_CODE: 212 | self._color = COLOR_TO_CODE[value] 213 | else: 214 | msg = f"unknown color {value!r}" 215 | raise ValueError(msg) 216 | 217 | @property 218 | def color_name(self) -> str: 219 | return CODE_TO_COLOR.get(self.color, "default") 220 | 221 | @property 222 | def kwargs(self) -> SessionData: 223 | return { 224 | "color": self.color_name, 225 | "debug": self.debug, 226 | "history": self.history, 227 | } 228 | 229 | def copy(self) -> Session: 230 | kw = self.kwargs 231 | kw["color"] = self._color 232 | s = Session(**kw) 233 | s.history = s.history.copy() 234 | return s 235 | 236 | def save(self, name: str = "") -> None: 237 | filename = name or datetime.now().strftime("%Y%m%d%H%M%S") 238 | data = json.dumps(self.kwargs) 239 | if not CACHE_DIR.exists(): 240 | CACHE_DIR.mkdir(parents=True, exist_ok=True) 241 | (CACHE_DIR / f"{filename}.json").write_text(data) 242 | 243 | @classmethod 244 | def load(cls, name: str) -> Session: 245 | return cls(**json.loads((CACHE_DIR / f"{name}.json").read_text())) 246 | 247 | @staticmethod 248 | def is_valid_name(name: str) -> bool: 249 | return all(char in Session.ALLOWED_CHARS for char in name) 250 | 251 | 252 | class REPL: 253 | def __init__(self, *, debug: bool) -> None: 254 | if not CONFIG_FILE.exists(): 255 | CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) 256 | CONFIG_FILE.touch() 257 | self.config = REPLConfig.load() 258 | cache_cleanup(self.config.session_lifetime) 259 | self.registry = Registry(globals()) 260 | self.session = Session( 261 | COLOR_TO_CODE.get(self.config.color, ""), 262 | debug=debug, 263 | ) 264 | 265 | def load_session(self, session: Session, *, reset_registry: bool = False) -> None: 266 | if reset_registry: 267 | self.registry = Registry(globals()) 268 | with ( 269 | Path("/dev/null").open("w") as dev_null, 270 | redirect_stdout(dev_null), 271 | redirect_stderr(dev_null), 272 | ): 273 | for stmt in session.history: 274 | core.run( 275 | stmt, 276 | self.registry, 277 | "", 278 | debug=session.debug, 279 | load_template=False, 280 | repl=True, 281 | ) 282 | self.session = session 283 | 284 | def finish(self) -> None: 285 | self.save() 286 | sys.exit() 287 | 288 | def save(self) -> None: 289 | if self.config.autosave: 290 | self.session.save() 291 | 292 | def handle_cmd(self, cmd: str, arg: str) -> str | None: 293 | if cmd in QUIT: 294 | if cmd.endswith("!"): 295 | sys.exit() 296 | self.finish() 297 | elif cmd in HELP: 298 | try: 299 | DAHLIA.print(HELP_TEXT_INDEX[arg].strip()) 300 | except KeyError: 301 | repl_err(f"unknown help subsection {arg!r}") 302 | elif cmd == "color": 303 | if arg == "save": 304 | self.config.color = self.session.color_name 305 | self.config.save() 306 | return None 307 | try: 308 | self.session.color = arg 309 | except ValueError as e: 310 | repl_err(str(e)) 311 | elif cmd == "debug": 312 | self.session.debug = not self.session.debug 313 | elif cmd in TIME: 314 | return arg 315 | elif cmd == "run": 316 | if not arg: 317 | repl_err("missing file") 318 | return None 319 | try: 320 | src = Path(arg).read_text() 321 | except FileNotFoundError: 322 | repl_err(f"file {arg!r} not found") 323 | return None 324 | core.run( 325 | src, self.registry, "", debug=self.session.debug, load_template=False 326 | ) 327 | elif cmd == "undo": 328 | s = self.session.copy() 329 | s.history.pop() 330 | self.load_session(s, reset_registry=True) 331 | elif cmd == "session": 332 | if not arg: 333 | repl_err("missing subcommand") 334 | return None 335 | try: 336 | subcmd, arg = arg.split() 337 | except ValueError: 338 | subcmd, arg = arg, "" 339 | if subcmd in NO_ARG_SESSION_SUBCMDS and arg: 340 | repl_err(f"expected no arguments for subcommand {subcmd!r}") 341 | return None 342 | if subcmd == "delete-all": 343 | sessions = tuple(CACHE_DIR.glob("*")) 344 | if not sessions: 345 | repl_err("no sessions to delete") 346 | return None 347 | if ( 348 | DAHLIA.input( 349 | "Are you sure you want to delete all" 350 | f" {len(sessions)} sessions? &7(Y/n) " 351 | ).casefold() 352 | == "n" 353 | ): 354 | return None 355 | size = 0 356 | for session in sessions: 357 | size += session.stat().st_size 358 | session.unlink() 359 | DAHLIA.print(f"&aRemoved {fmt_size(size)} of session files") 360 | elif subcmd == "autosave": 361 | if arg: 362 | if (arg := arg.casefold()) not in ("true", "false"): 363 | repl_err(f"invalid value {arg!r} (must be true/false)") 364 | return None 365 | self.config.autosave = arg == "true" 366 | self.config.save() 367 | if self.config.autosave: 368 | DAHLIA.print("&aAutosave enabled") 369 | else: 370 | DAHLIA.print("&cAutosave disabled") 371 | elif subcmd == "list": 372 | total_size = 0 373 | term_size = min(get_terminal_size().columns - 8, 48) 374 | for session in CACHE_DIR.glob("*"): 375 | size = session.stat().st_size 376 | total_size += size 377 | DAHLIA.print( 378 | f"{truncate(session.name, term_size)} &e({fmt_size(size)})" 379 | ) 380 | DAHLIA.print("-" * term_size) 381 | DAHLIA.print(f"&l{'Total':<{term_size + 1}} &e{fmt_size(total_size)}\n") 382 | elif subcmd == "save": 383 | if not Session.is_valid_name(arg): 384 | repl_err( 385 | f"invalid session name {arg!r} " 386 | "(allowed chars: A..Z a..z 0..9 _ -])" 387 | ) 388 | return None 389 | if (CACHE_DIR / f"{arg}.json").exists() and input( 390 | f"Session {arg!r} already exists. Replace? (y/N) " 391 | ).casefold() != "y": 392 | return None 393 | self.session.save(arg) 394 | DAHLIA.print(f"Saved session {arg!r}") 395 | elif subcmd == "load": 396 | if not Session.is_valid_name(arg): 397 | repl_err( 398 | f"invalid session name {arg!r} " 399 | "(allowed chars: A..Z a..z 0..9 _ -])" 400 | ) 401 | return None 402 | if not (CACHE_DIR / f"{arg}.json").exists(): 403 | repl_err(f"session {arg!r} not found") 404 | return None 405 | self.load_session(Session.load(arg)) 406 | elif subcmd == "lifetime": 407 | if not arg: 408 | lifetime = str(self.config.session_lifetime).removesuffix(".0") 409 | DAHLIA.print(f"Current session lifetime is {lifetime} days") 410 | return None 411 | try: 412 | self.config.session_lifetime = float(arg) 413 | except ValueError: 414 | repl_err(f"invalid lifetime {arg!r} (must be a float)") 415 | return None 416 | self.config.save() 417 | DAHLIA.print(f"&aUpdated session lifetime to {arg} days") 418 | elif subcmd == "restore": 419 | try: 420 | most_recent_session = max(unnamed_sessions(), key=lambda p: p[1]) 421 | except ValueError: 422 | repl_err("no sessions to restore") 423 | return None 424 | self.load_session(Session.load(most_recent_session[0].stem)) 425 | DAHLIA.print(f"&aRestored latest session ({most_recent_session[1]})") 426 | else: 427 | repl_err(f"unknown session subcommand {subcmd!r}") 428 | elif cmd == "clear": 429 | print("\033[2J\033[H", end="") 430 | else: # :3 431 | DAHLIA.print("&dnya~") 432 | return None 433 | 434 | def read(self) -> str: 435 | stmt = "" 436 | prompt = PS1 437 | while True: 438 | stmt += DAHLIA.input(self.session.color + prompt) 439 | if not stmt: 440 | continue 441 | stmt += ";" 442 | try: 443 | error, stack = match_brackets(tokenize(stmt, repl=True)) 444 | except CrossandraError as e: 445 | if '"' in str(e): 446 | error, stack = 1, [] 447 | else: 448 | handle_exception(SamariumSyntaxError(str(e))) 449 | raise RuntimeError from None 450 | 451 | if not error: 452 | return stmt 453 | if error == -1: 454 | handle_exception( 455 | SamariumSyntaxError( 456 | f"missing opening bracket for {stack[-1].value}" 457 | ) 458 | ) 459 | raise RuntimeError from None 460 | 461 | prompt = PS2 462 | stmt = stmt[:-1] + "\n" 463 | 464 | def run(self) -> None: 465 | DAHLIA.print( 466 | f"{self.session.color}Samarium {__version__}" 467 | + " [DEBUG]" * self.session.debug 468 | ) 469 | Runtime.repl = True 470 | while True: 471 | try: 472 | time_code = None 473 | stmt = self.read() 474 | if is_command(stmt): 475 | cmd = stmt[1:-1] 476 | if not cmd: 477 | repl_err("missing command") 478 | continue 479 | cmd, _, arg = cmd.partition(" ") 480 | if cmd not in COMMANDS: 481 | repl_err(f"unknown command {cmd!r}") 482 | continue 483 | time_code = self.handle_cmd(cmd, arg) 484 | if time_code is None: 485 | continue 486 | time_code += ";" 487 | elif not stmt.strip("; ").isidentifier(): 488 | self.session.history.append(stmt) 489 | start = time() 490 | core.run( 491 | time_code or stmt, 492 | self.registry, 493 | "", 494 | debug=self.session.debug, 495 | load_template=False, 496 | repl=True, 497 | ) 498 | if time_code: 499 | DAHLIA.print(f"&2{time() - start:.3f} seconds&R") 500 | self.registry.output = "" 501 | except RuntimeError: 502 | pass 503 | except KeyboardInterrupt: 504 | print() 505 | except (EOFError, SystemExit): 506 | break 507 | self.finish() 508 | 509 | 510 | def cache_cleanup(lifetime: float) -> None: 511 | threshold = datetime.now() - timedelta(days=lifetime) 512 | for file, date in unnamed_sessions(): 513 | if date < threshold: 514 | file.unlink() 515 | 516 | 517 | def fmt_size(size: float) -> str: 518 | units = iter("KMGTP") 519 | u = "" 520 | while size > 1000: 521 | size /= 1000 522 | u = next(units) 523 | return f"{size:.1f}{u}B" 524 | 525 | 526 | def is_command(stmt: str) -> bool: 527 | if len(stmt) > 1: 528 | return stmt[0] == ":" != stmt[1] 529 | return True 530 | 531 | 532 | def repl_err(msg: str) -> None: 533 | DAHLIA.print(f"&4[REPLError] {msg}", file=sys.stderr) 534 | 535 | 536 | def truncate(string: str, length: int) -> str: 537 | if len(string) > length: 538 | return string[: length - 3] + "..." 539 | return string.ljust(length) 540 | 541 | 542 | def unnamed_sessions() -> Iterator[tuple[Path, datetime]]: 543 | for file in CACHE_DIR.glob("*"): 544 | with suppress(ValueError): 545 | yield (file, datetime.strptime(file.stem, "%Y%m%d%H%M%S")) 546 | -------------------------------------------------------------------------------- /samarium/runtime.py: -------------------------------------------------------------------------------- 1 | class Runtime: 2 | repl = False 3 | -------------------------------------------------------------------------------- /samarium/template.txt: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | __file__ = "{{SOURCE}}" 4 | {{CODE}} 5 | if __name__ == "samarium": 6 | try: 7 | entry 8 | except NameError: 9 | sys.exit() 10 | argc = entry.special() 11 | is_class = isinstance(entry, type) 12 | if is_class: 13 | argc -= 1 14 | if not argc.val: 15 | ex = entry() 16 | elif argc.val == 1: 17 | ex = entry(Array(map(String, sys.argv[1:]))) 18 | else: 19 | raise exceptions.SamariumSyntaxError( 20 | "entry function should take 0 or 1 arguments" 21 | ) 22 | if not is_class: 23 | sys.exit(ex.val) 24 | -------------------------------------------------------------------------------- /samarium/tokenizer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import sys 5 | from typing import Union, cast 6 | 7 | from crossandra import Crossandra, CrossandraError, Rule, common 8 | 9 | from samarium.exceptions import SamariumSyntaxError, handle_exception 10 | from samarium.tokens import Token 11 | from samarium.utils import convert_float 12 | 13 | 14 | def to_number(string: str) -> int | float: 15 | string = string.replace("/", "1").replace("\\", "0") 16 | if string == ".": 17 | string = "0." 18 | return convert_float(string, base=2, sep="`") 19 | 20 | 21 | Tokenlike = Union[Token, str, int] 22 | 23 | SM_BIT = r"[\\\/]" 24 | 25 | crossandra = Crossandra( 26 | Token, 27 | ignore_whitespace=True, 28 | rules=[ 29 | Rule(r"==<.*>==", flags=re.M | re.S, ignore=True), 30 | Rule(r"==[^\n]*", flags=re.M | re.S, ignore=True), 31 | Rule( 32 | common.DOUBLE_QUOTED_STRING.pattern, 33 | lambda x: x.replace("\n", r"\n"), 34 | flags=re.S, 35 | ), 36 | Rule(rf"{SM_BIT}+`?{SM_BIT}*|`{SM_BIT}*", to_number), 37 | Rule(r"\w+"), 38 | ], 39 | ) 40 | 41 | 42 | def tokenize(code: str, *, repl: bool = False) -> list[Tokenlike]: 43 | try: 44 | return cast(list[Tokenlike], crossandra.tokenize(code)) 45 | except CrossandraError as e: 46 | if repl: 47 | raise 48 | errmsg = str(e) 49 | if '"' in errmsg: 50 | errmsg = "unclosed string literal" 51 | handle_exception(SamariumSyntaxError(errmsg)) 52 | sys.exit() 53 | -------------------------------------------------------------------------------- /samarium/tokens.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Token(Enum): 5 | # Arithmetic 6 | ADD = "+" 7 | SUB = "-" 8 | MUL = "++" 9 | DIV = "--" 10 | POW = "+++" 11 | MOD = "---" 12 | 13 | # Comparison 14 | GE = ">:" 15 | GT = ">" 16 | LE = "<:" 17 | LT = "<" 18 | EQ = "::" 19 | NE = ":::" 20 | 21 | # Logical and Membership 22 | AND = "&&" 23 | IN = "->?" 24 | NOT = "~~" 25 | OR = "||" 26 | XOR = "^^" # TODO(trag1c): Implement in ≤2025 (optional) 27 | 28 | # Bitwise 29 | BAND = "&" 30 | BNOT = "~" 31 | BOR = "|" 32 | BXOR = "^" 33 | 34 | # Parens, Brackets and Braces 35 | BRACKET_OPEN = "[" 36 | BRACKET_CLOSE = "]" 37 | 38 | BRACE_OPEN = "{" 39 | BRACE_CLOSE = "}" 40 | 41 | PAREN_OPEN = "(" 42 | PAREN_CLOSE = ")" 43 | 44 | TABLE_OPEN = "{{" 45 | TABLE_CLOSE = "}}" 46 | 47 | # Control Flow 48 | CATCH = "!!" 49 | ELSE = ",," 50 | FOR = "..." 51 | FROM = "<-" 52 | IF = "?" 53 | IMPORT = "<=" 54 | THROW = "!!!" 55 | TO = "->" 56 | TRY = "??" 57 | WHILE = ".." 58 | 59 | # OOP / Functions 60 | CLASS = "@" 61 | DATACLASS = "@!" 62 | DEFAULT = "<>" 63 | FUNCTION = "*" 64 | INSTANCE = "'" 65 | ENTRY = "=>" 66 | YIELD = "**" 67 | 68 | # Slicing 69 | SLICE_OPEN = "<<" 70 | SLICE_CLOSE = ">>" 71 | 72 | # Object Manipulation 73 | CAST = "%" 74 | SPECIAL = "$" 75 | EXIT = "=>!" 76 | HASH = "##" 77 | PARENT = "!?" 78 | TYPE = "?!" 79 | READLINE = "???" 80 | PRINT = "!" 81 | 82 | # File I/O 83 | FILE_CREATE = "?~>" 84 | FILE_APPEND = "&~~>" 85 | FILE_READ = "<~~" 86 | FILE_WRITE = "~~>" 87 | FILE_READ_WRITE = "<~>" 88 | FILE_BINARY_APPEND = "&%~>" 89 | FILE_BINARY_READ = "<~%" 90 | FILE_BINARY_WRITE = "%~>" 91 | FILE_BINARY_READ_WRITE = "<%>" 92 | FILE_QUICK_APPEND = "&~>" 93 | FILE_QUICK_READ = "<~" 94 | FILE_QUICK_WRITE = "~>" 95 | FILE_QUICK_BINARY_APPEND = "&%>" 96 | FILE_QUICK_BINARY_READ = "<%" 97 | FILE_QUICK_BINARY_WRITE = "%>" 98 | 99 | # Other 100 | ENUM = "#" 101 | ASSIGN = ":" 102 | ATTR = "." 103 | UNIX_STMP = "@@" 104 | ARR_STMP = "@@@" 105 | END = ";" 106 | SEP = "," 107 | SLEEP = ",.," 108 | ZIP = "><" 109 | 110 | 111 | FILE_IO_TOKENS = [ 112 | token for name, token in Token.__members__.items() if name.startswith("FILE_") 113 | ] 114 | 115 | OPEN_TOKENS = [ 116 | Token.BRACKET_OPEN, 117 | Token.BRACE_OPEN, 118 | Token.PAREN_OPEN, 119 | Token.TABLE_OPEN, 120 | Token.SLICE_OPEN, 121 | ] 122 | 123 | CLOSE_TOKENS = [ 124 | Token.BRACKET_CLOSE, 125 | Token.BRACE_CLOSE, 126 | Token.PAREN_CLOSE, 127 | Token.TABLE_CLOSE, 128 | Token.SLICE_CLOSE, 129 | ] 130 | -------------------------------------------------------------------------------- /samarium/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from re import sub 4 | from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar 5 | 6 | from samarium.exceptions import SamariumTypeError, SamariumValueError 7 | 8 | if TYPE_CHECKING: 9 | from collections.abc import Callable 10 | 11 | __version__ = "0.6.2" 12 | 13 | T = TypeVar("T") 14 | 15 | 16 | KT = TypeVar("KT") 17 | VT = TypeVar("VT") 18 | 19 | 20 | class ClassProperty(Generic[T]): 21 | def __init__(self, func: Callable[..., T]) -> None: 22 | self.func = func 23 | 24 | def __get__(self, obj: object, owner: object = None) -> T: 25 | if obj is None: 26 | obj = owner 27 | return self.func(obj) 28 | 29 | 30 | class Singleton: 31 | _instances: ClassVar[dict[type[Singleton], Singleton]] = {} 32 | 33 | def __new__(cls, *args: Any, **kwargs: Any) -> Singleton: 34 | if cls not in cls._instances: 35 | cls._instances[cls] = super().__new__(cls, *args, **kwargs) 36 | return cls._instances[cls] 37 | 38 | 39 | def sysexit(*args: Any) -> None: 40 | if len(args) > 1: 41 | msg = "=>! only takes one argument" 42 | raise SamariumTypeError(msg) 43 | code = args[0].val if args else 0 44 | if not isinstance(code, (int, str)): 45 | msg = "=>! only accepts integers and strings" 46 | raise SamariumTypeError(msg) 47 | raise SystemExit(code) 48 | 49 | 50 | def convert_float(string: str, *, base: int, sep: str = ".") -> int | float: 51 | exp_sep = "p" if base == 16 else "e" 52 | if not (sep in string or exp_sep in string): 53 | return int(string, base) 54 | float_part, _, exp_part = string.partition(exp_sep) 55 | int_, _, dec = float_part.partition(sep) 56 | out = int(int_ or "0", base) + sum( 57 | int(v, base) * base**~i for i, v in enumerate(dec) 58 | ) 59 | if exp_part: 60 | try: 61 | exponent = int(exp_part, 10 if base == 16 else base) 62 | except ValueError: 63 | msg = f"invalid exponent: {exp_part}" 64 | raise SamariumValueError(msg) from None 65 | out *= (2 if base == 16 else base) ** exponent 66 | return out 67 | 68 | 69 | def parse_number(string: str) -> tuple[int | float, bool]: 70 | string = string.strip() 71 | orig = string 72 | neg = len(string) 73 | b = "d" 74 | if ":" in string: 75 | b, _, string = string.partition(":") 76 | if b not in "box": 77 | msg = f"{b} is not a valid base" 78 | raise SamariumValueError(msg) 79 | base = {"b": 2, "o": 8, "x": 16, "d": 10}[b] 80 | string = string.lstrip("-") 81 | neg = -2 * ((neg - len(string)) % 2) + 1 82 | 83 | try: 84 | num = neg * convert_float(string, base=base) 85 | except ValueError: 86 | no_prefix = orig[2:] if orig[1] == ":" else orig 87 | msg = f'invalid string for Number with base {base}: "{no_prefix}"' 88 | raise SamariumValueError(msg) from None 89 | else: 90 | return num, isinstance(num, int) or num.is_integer() 91 | 92 | 93 | def smformat(string: str, fields: str | list[Any] | dict[Any, Any]) -> str: 94 | if isinstance(fields, str): 95 | fields = [fields] 96 | it = fields.items() if isinstance(fields, dict) else enumerate(fields) 97 | for k, v in it: 98 | string = sub(rf"(? str: 103 | return obj.__name__.removeprefix("sm_") 104 | 105 | 106 | def get_type_name(obj: Any) -> str: 107 | return type(obj).__name__.removeprefix("sm_") 108 | --------------------------------------------------------------------------------