diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7e746513 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,297 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.4.0] - 2022-12-01 + +### Added +- `Array%` +- `Enum%` +- `io` module: + - `io.Bytes` + - `io.inputcast` + - `io.read_until` +- `iter.cycle` by [@Lunarmagpie](https://github.com/Lunarmagpie) +- New import system, including: + - import aliases + - inline imports +- New special method syntax +- Partial Python Interoperability by [@Endercheif](https://github.com/Endercheif) +- `start` parameter for `math.sum`, thus allowing to sum non-Integers +- Static methods (`~'*` keyword) +- `string.split()` now supports separators of length greater than 1, and also handles empty separators +- Strings of length greater than 1 can now be cast into Arrays of Integers +- Subtracting strings +- Support for `^L`, `^[[A`, `^[[B`, `^[[C`, `^[[D` in the REPL +- `to_bit` methods for: + - `Enum` + - `File` + - `Iterator` + - `Module` +- `Type`s and functions are now hashable +- Zip `><` operator +- `Zip` type + +### Changed +- Flipped argument order for: + - `iter.map` + - `iter.filter` + - `iter.filter_false` +- Greatly improved error messages +- Improved `collections.Set` methods: + - Replaced `union` with `|` + - Replaced `intersection` with `&` + - Replaced `difference` with `-` + - Removed `is_subset` in place of `::`, `:::`, `>`, `<`, `>:`, `<:` operator support +- Improved function to string conversion +- Improved implicit null detection +- Improved readline error handling +- Improved slice object detection +- Improved string integer parsing +- Improved variable type verification +- Rewrote the objects (should be 10–50% faster than Samarium 0.3.1 πŸš€) +- Replaced the native tokenizer with a [crossandra](https://github.com/trag1c/crossandra) tokenizer (~3x faster tokenization πŸš€) +- Updated `to_string` methods of: + - `collections.ArithmeticArray` + - `collections.Deque` + - `collections.Queue` + - `collections.Set` + +### Fixed +- `collections.Set.#new_set_size` now takes unsized sets into consideration +- Constructing `Slice`s from `Type` now correctly works +- Error message shown when trying to run an unknown file is now written to stderr, not stdout +- Syntax errors now cannot be caught + +### Removed +- `collections.Set.values()` (use `collections.Set.items`) +- English special method names +- `iter.zip` (use the `><` operator) +- `iter.enumerate` (use the `><` operator with `<<>>`) +- Some dead code :) +- `string.format` (use `String`'s `---` operator) + +--- + +Also big thanks to [@qexat](https://github.com/qexat) & [@Celeo](https://github.com/Celeo) for code improvements! ❀️ + +## [0.3.1] - 2022-09-23 + +### Fixed +- Added missing cases for implicit null + +### Removed +- Unused imports & variables in the standard library +- Unused slots in built-in types + + +## [0.3.0] - 2022-09-21 + +### Added +- `**` (the yield statement) +- Array support for `string.format` +- Better error messages +- `datetime` module by [@Endercheif](https://github.com/Endercheif) +- Enums +- `iter.chunks` +- `iter.flatten` +- `iter.sorted` +- Iterators +- Memory address lookup +- Private variables +- snake_case support +- Substring support for `iter.find_all` + +### Changed +- `#` for assert has been replaced by `!!` +- `@@` now returns the Unix timestamp +- `@@@` now returns the date and time array +- Classes can now be used as entry points +- Improved hashing speed +- Improved `iter` module speed by making it use Iterators over Arrays +- Moved from [termcolor](https://pypi.org/project/termcolor/) to [Dahlia](https://github.com/trag1c/Dahlia) +- Rewrote the transpiler +- Slice objects are now iterable and can be used as ranges (about 3–7x faster than `iter.range` πŸš€) +- Slices now use `..` instead of `**` for the step delimiter (`<>` would be `<>`) +- Class special method names were changed to snake_case +- Standard Library function names were changed to snake_case +- Standard Library constant names were changed to SCREAMING_SNAKE_CASE +- `to_string` method doesn't need to be defined for the object to be printable (defaults to ``) + +### Fixed +- Nested slicing + +### Removed +- `_` as a null token +- `iter.range` (use slices) +- `iter.sort` (use `iter.sorted`) + + +## [0.2.3] - 2022-06-20 + +### Fixed +- Fixed converting slices to strings + + +## [0.2.2] - 2022-06-18 + +### Fixed +- Added a missing `argc` attribute for the template + + +## [0.2.1] - 2022-06-18 + +### Fixed +- Class methods now correctly pass the instance + + +## [0.2.0] - 2022-06-13 + +### Added +- `-h` / `--help` option +- Argument unpacking +- Decorators +- Enriched modules: + - `collections.ArithmeticArray` + - `iter`: + - `all` + - `any` + - `findAll` + - `pairwise` + - `zip` + - `zipLongest` + - `math.isPrime` + - `random.randint` + - `string.format` + - `types`: + - `Array` + - `Integer` + - `Null` + - `Slice` + - `String` + - `Table` + - `UUID4` +- File descriptor support for standard stream access +- Greatly improved object constructors for: + - `Array`: + - Default value: `[]` + - `Array(a)` will now return a copy of the array `a` + - Arrays can now be constructed from strings and tables: + - `Array("ball") :: ["b", "a", "l", "l"]` + - `Array({{// -> /\\/, "X" -> "D"}}) :: [[//, /\\/], ["X", "D"]]` + - `Integer`: + - Default value: `\` + - Added support for binary, octal, hexadecimal representations: + - `Integer("b:1000") :: Integer("8")` + - `Integer("o:1000") :: Integer("512")` + - `Integer("x:1000") :: Integer("4096")` + - `String`: + - Default value: `""` + - `String(s)` will now return a copy of the string `s` + - `Table`: + - Default value: `{{}}` + - `Table(t)` will now return a copy of the table `t` + - Tables can be constructed from arrays containing 2-element iterables: + - `Table([[//, /\\/], "XD"]) :: {{// -> /\\/, "X" -> "D"}}` +- Introduced a new `Function` class for functions, replacing the old decorator +- New function syntax: + - `arg?` now makes the argument optional + - optional arguments require setting the default value using the `arg <> default` statement inside the function body + - `args...` will now accept a variable number of arguments and pass them to the function as an array +- New `operator` module - containing standard operators as functions: + - `add` (`x + y`) + - `and` (`x & y`) + - `cast` (`x%`) + - `divide` (`x -- y`) + - `equals` (`x :: y`) + - `greaterThanOrEqual` (`x >: y`) + - `greaterThan` (`x > y`) + - `has` (`y ->? x`) + - `hash` (`x##`) + - `lessThanOrEqual` (`x <: y`) + - `lessThan` (`x < y`) + - `mod` (`x --- y`) + - `multiplty` (`x ++ y`) + - `not` (`~x`) + - `notEquals` (`x ::: y`) + - `or` (`x | y`) + - `power` (`x +++ y`) + - `random` (`x??`) + - `special` (`x$`) + - `subtract` (`x - y`) + - `toBit` + - `toString` + - `xor` (`x ^ y`) +- Project description in `pyproject.toml` +- Samarium REPL +- Separate error for IO operations: `IOError` +- Some objects now have a `.random` method, supported by the `object??` syntax: + - `array??` will return a random element + - `integer??` will return: + - a number in range `[0, n)` for positive values + - a number in range `[n, 0)` for negative values + - `0` for `0??` + - `slice??` will return a number from an attribute-based range + - `string??` will return a random character + - `table??` will return a random key +- Shebang support +- Special method for functions: + - `function$` will now return the number of parameters the function accepts +- While loop condition is now optional (`.. {}` is equivalent to `.. / {}`) + +### Changed +- `<>` now serves for setting the default parameter value +- Bumped the minimum Python version to 3.9 +- Classes no longer need the `create` method defined to be initializable. +- Improved scopes and empty body handling +- Improved and added new error messages, such as: + - Invalid table keys + - Unbalanced brackets + - Too many parameters for the main function (or the lack of one) +- Improved object speed: + - `Array`s β€” up to 30% faster + - `Integer`s β€” up to 2.4x faster + - `Null` objects β€” up to 2.2x faster + - `String`s β€” up to 40% faster + - `Table`s β€” up to 25% faster +- Improved transpiling safety +- Improved internal typechecking accuracy +- RNG syntax (`^^x -> y^^`) was replaced by the new random `object??` syntax +- `Slice`s can now be instantiated as standalone objects +- Various refactorings + +### Fixed +- Fixed `-c` option not tokenizing some statements properly, such as: + - statements starting with a table definition + - statements ending with a `!` +- Fixed `string.join` for empty delimiters +- Fixed `string.pad` doubling when `string$` was equal to `length` +- Fixed exception name categorization +- Fixed `string.replace` +- Functions defined inside class methods no longer need an instance +- Fixed false positives for multiple type verification +- Fixed Integer construction for cross-type comparison + +### Removed +- `->` operator support for assert (`#`) +- Constants +- Default parameter value in the function definition +- `math.fromDecimal` - use `types.Integer(string)` instead +- `random.choice` - use `iterable??` instead + + +## [0.1.0] - 2022-03-05 + +Initial release πŸš€ + +[0.1.0]: https://github.com/samarium-lang/Samarium/releases/tag/0.1.0 +[0.2.0]: https://github.com/samarium-lang/Samarium/compare/0.1.0...0.2.0 +[0.2.1]: https://github.com/samarium-lang/Samarium/compare/0.2.0...0.2.1 +[0.2.2]: https://github.com/samarium-lang/Samarium/compare/0.2.1...0.2.2 +[0.2.3]: https://github.com/samarium-lang/Samarium/compare/0.2.2...0.2.3 +[0.3.0]: https://github.com/samarium-lang/Samarium/compare/0.2.3...0.3.0 +[0.3.1]: https://github.com/samarium-lang/Samarium/compare/0.3.0...0.3.1 +[0.4.0]: https://github.com/samarium-lang/Samarium/compare/0.3.1...0.4.0 \ No newline at end of file diff --git a/README.md b/README.md index 7b7a64b5..c73c71ea 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,6 @@ Special thanks to: - [tetraxile](https://github.com/tetraxile) for helping with design choices and writing the docs - [MithicSpirit](https://github.com/MithicSpirit) for making an AUR package for Samarium - [DarviL82](https://github.com/DarviL82) for fixing some issues -- [Endercheif](https://github.com/Endercheif) for making the documentation look fancy and helping with design choices +- [Endercheif](https://github.com/Endercheif) for making the documentation look fancy, helping with design choices, and adding partial Python Interoperability If you have any questions, or would like to get in touch, join the [Discord server](https://discord.gg/C8QE5tVQEq)! diff --git a/docs/arrays.md b/docs/arrays.md index 46f36eb6..a24efc13 100644 --- a/docs/arrays.md +++ b/docs/arrays.md @@ -14,4 +14,8 @@ Items can also be removed from an array using the `-` operator, either by index `["a", "b", "c"] - /` gives `["a", "c"]` -`["a", "b", "c", "d"] - ["b", "d"]` gives `["a", "c"]` \ No newline at end of file +`["a", "b", "c", "d"] - ["b", "d"]` gives `["a", "c"]` + +Arrays of Integers can be casted to Strings: + +`[//\/\\\, //\/\\/, ////\\/, //\\\\/]%` gives `"hiya"` \ No newline at end of file diff --git a/docs/builtins.md b/docs/builtins.md index 5233179d..94b2a5e9 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -10,10 +10,12 @@ It will read until it receives a newline character. A prompt can be given by preceding the `???` with a string, for example `"input: "???`. +> Related library: [`io`](stdio.md) + ## PRINTLN -Objects can be written to standard output by appending a `!` character to them. +Objects can be written to standard output by adding an `!` at the end. Note that they won't be written exactly as they would appear in Samarium. `"a"!` will write `a` to standard output. @@ -72,15 +74,6 @@ These instances are callable and can be used to convert values into that type, l > Related library: [`types`](stdtypes.md) -## CAST - -The cast function `%` can convert between a Unicode character (a string) and its corresponding code (an integer). - -`"a"%` returns `97`. - -`/\\\\/%` returns `"!"`. - - ## SPECIAL The special method `$` has different uses depending on the type of object it's used on. @@ -109,11 +102,14 @@ The timestamp function `@@` returns the time in milliseconds since the epoch. !!! note Timezone is included. For UTC timestamp, use `datetime.timestamp_utc`. +> Related library: [`datetime`](stddatetime.md) + ## DTNOW The dtnow function `@@@` gets the system's current date and time as an array of integers, in the format `[year, month, day, hour, minute, second, millisecond, utc_hour_offset, utc_minute_offset]`. +> Related library: [`datetime`](stddatetime.md) ## SLEEP @@ -124,6 +120,8 @@ The sleep function `,.,` pauses execution for the specified number of millisecon == sleep for 1000 milliseconds (1 second) ``` +> Related library: [`datetime`](stddatetime.md) + ## ASSERT @@ -141,7 +139,7 @@ will raise `[AssertionError] error message`. ## PARENT The parent function `!?` gets the parent/inherited classes of the input class. -If a class only has one parent, it will be return directly. +If a class only has one parent, it will be returned directly. If a class has multiple parents, they will be returned in an array, in the same order as in the class definition. Note that this will only go one layer deep, i.e. if class `A` has parent `B` and class `B` has parent `C`, `A!?` will only return `B`, and `A!?!?` will return `C`. diff --git a/docs/classes.md b/docs/classes.md index dcb1f264..39be4afe 100644 --- a/docs/classes.md +++ b/docs/classes.md @@ -17,7 +17,7 @@ Just like variables, class attributes can be made private by prefixing them with @ Foo { shared: []; - create var * { + => var * { 'var: var; '#pv: var - /; } @@ -65,48 +65,39 @@ These methods are as follows (where `func(...)` indicates a variable number of a Function | Python | Use --- | --- | --- -`add(other)` | `add` | Interacts with the addition operator `+`. -`add_assign(other)` | `iadd` | Interacts with the addition assignment operator `+:`.
`x+: y` is equivalent to `x.add_assign(y)` (or `x: x + y`). -`and(other)` | `and` | Interacts with the bitwise AND operator `&`. -`and_assign(other)` | `iand` | Interacts with the bitwise AND assignment operator `&:`. -`call(...)` | `call` | Called when an instance itself is "called" as a function;
`x(...)` roughly translates to `x?!.call(x, ...)`. -`cast()` | -- | Interacts with the cast function character `%`. -`create(...)` | `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. -`divide(other)` | `floordiv` | Interacts with the division operator `--`. -`divide_assign(other)` | `ifloordiv` | Interacts with the division assignment operator `--:`. -`equals(other)` | `eq` | Implements the equality operator `::`. -`get_item(index)` | `getitem` | Implements indexing an object;
`x<>` is equivalent to `x.get_item(index)`. -`greater_than(other)` | `gt` | Implements the greater than operator `>`. -`greater_than_or_equal(other)` | `ge` | Implements the greater than or equal operator `>:`. -`has(item)` | `contains` | Implements membership testing,
returns `1` (object contains `item`)
or `0` (object does not contain `item`).
Interacts with `->?` operator. -`hash()` | `hash` | Called by the built-in hash function `##`,
and for keys in a table.
Objects which compare equal
should have the same hash value. -`iterate()` | `iter` | Called when iterating over an object in a `foreach` loop.
Returns an array of objects to iterate over. -`less_than(other)` | `lt` | Implements the less than operator `<`. -`less_than_or_equal(other)` | `le` | Implements the less than or equal operator `<:`. -`mod(other)` | `mod` | Interacts with the modulo operator `---`. -`mod_assign(other)` | `imod` | Interacts with the modulo assignment operator `---:`. -`multiply(other)` | `mul` | Interacts with the multiplication operator `++`. -`multiply_assign(other)` | `imul` | Interacts with the multiplication
assignment operator `++:`. -`negative()` | `neg` | Interacts with the negative unary operator `-`. -`not()` | `invert` | Interacts with the bitwise NOT operator `~`. -`not_equals(other)` | `ne` | Implements the inequality operator `:::`. -`or(other)` | `or` | Interacts with the bitwise OR operator `\|`. -`or_assign(other)` | `ior` | Interacts with the bitwise OR assignment operator `\|:`. -`positive()` | `pos` | Interacts with the positive unary operator `+`. -`power(other)` | `pow` | Interacts with the exponentiation operator `+++`. -`power_assign(other)` | `ipow` | Interacts with the exponentiation
assignment operator `+++:`. -`set_item(index, value)` | `setitem` | Implements assigning to an index of an object;
`x<>: value` is equivalent
to `x.set_item(index, value)`. -`special()` | -- | Interacts with the special function character `$`. -`subtract(other)` | `sub` | Interacts with the subtraction operator `-`. -`subtract_assign(other)` | `isub` | Interacts with the subtraction assignment operator `-:`. -`to_bit()` | `bool` | Implements boolean value testing,
returns `1` (truthy) or `0` (falsy).
Used for conditional statements and logical operators. -`to_string()` | `str` | Returns the string representation of an object. -`xor(other)` | `xor` | Interacts with the bitwise XOR operator `^`. -`xor_assign(other)` | `ixor` | Interacts with the bitwise XOR assignment operator `^:`. +`+(other)` | `add` | Interacts with the addition operator `+`. +`&(other)` | `and` | Interacts with the bitwise AND operator `&`. +`()(...)` | `call` | Called when an instance itself is "called" as a function;
`x(...)` roughly translates to `x?!.call(x, ...)`. +`%()` | -- | Interacts with the cast function character `%`. +`=>(...)` | `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. +`--(other)` | `floordiv` | Interacts with the division operator `--`. +`::(other)` | `eq` | Implements the equality operator `::`. +`<<>>(index)` | `getitem` | Implements indexing an object;
`x<>` is equivalent to `x.get_item(index)`. +`>(other)` | `gt` | Implements the greater than operator `>`. +`>:(other)` | `ge` | Implements the greater than or equal operator `>:`. +`->?(item)` | `contains` | Implements membership testing,
returns `1` (object contains `item`)
or `0` (object does not contain `item`).
Interacts with `->?` operator. +`##()` | `hash` | Called by the built-in hash function `##`,
and for keys in a table.
Objects which compare equal
should have the same hash value. +`...()` | `iter` | Called when iterating over an object in a `foreach` loop.
Returns an array of objects to iterate over. +`<(other)` | `lt` | Implements the less than operator `<`. +`<:(other)` | `le` | Implements the less than or equal operator `<:`. +`---(other)` | `mod` | Interacts with the modulo operator `---`. +`++(other)` | `mul` | Interacts with the multiplication operator `++`. +`-_()` | `neg` | Interacts with the negative unary operator `-`. +`~()` | `invert` | Interacts with the bitwise NOT operator `~`. +`:::(other)` | `ne` | Implements the inequality operator `:::`. +`|(other)` | `or` | Interacts with the bitwise OR operator `\|`. +`+_()` | `pos` | Interacts with the positive unary operator `+`. +`+++(other)` | `pow` | Interacts with the exponentiation operator `+++`. +`<<>>:(index, value)` | `setitem` | Implements assigning to an index of an object;
`x<>: value` is equivalent
to `x.set_item(index, value)`. +`$()` | -- | Interacts with the special function character `$`. +`-(other)` | `sub` | Interacts with the subtraction operator `-`. +`?()` | `bool` | Implements boolean value testing,
returns `1` (truthy) or `0` (falsy).
Used for conditional statements and logical operators. +`!()` | `str` | Returns the string representation of an object. +`^(other)` | `xor` | Interacts with the bitwise XOR operator `^`. -Two special methods – `create` and `to_string` – have default definitions: +Two special methods – `=>` and `!` – have default definitions: ```sm @ Foo {} @@ -118,24 +109,38 @@ Two special methods – `create` and `to_string` – have default definitions: The above class definition is equivalent to: ```sm @ Foo { - create * {} + => * {} - to_string * { - <-string.format; - * format("<$name@$address>", {{"name" -> '?!, "address" -> '**}}); + ! * { + * "<$name@$address>" --- {{"name" -> '?!, "address" -> '**}}; } } ``` -Some of the comparison operators can be inferred from others, so not all of them are necessary to provide implementations for. -The specific operators needed to infer each comparison operator are listed in the following table: +Some of the comparison operators can be inferred from others, +so not all of them are necessary to provide implementations for. +The following operators infer from each other: +- `::` and `:::` +- `>` and `<` +- `>:` and `<:` -Operator | Inferred from ---- | --- -`:::` | `::` -`<` | `>` and `::` -`<:` | `>` -`>:` | `>` and `::` + +## Static Methods +Methods can be made static by replacing the `*` keyword with the `~'*` keyword +(where `~'` can be read as "no instance"): +```sm +<=calendar.date; + +@ Calendar { + is_weekend date ~'* { + * date.weekday > /\\; + } +} + +=> * { + Calendar.is_weekend(date("2022-11-08"))!; == 0 +} +``` ## Classes As Entry Points @@ -148,7 +153,7 @@ A class named `=>` can serve as an entry point instead of a function: ``` ```sm @ => { - create argv * { + => argv * { "Hello, " + argv<>!; } } @@ -161,12 +166,12 @@ Decorators can also be created using classes: ```sm @ OutputStorage { - create func * { + => func * { 'func: func; 'outputs: []; } - call args... * { + () args... * { out: 'function(**args); 'outputs_: [out]; * out; diff --git a/docs/controlflow.md b/docs/controlflow.md index ebaf91f2..7ac16aca 100644 --- a/docs/controlflow.md +++ b/docs/controlflow.md @@ -40,7 +40,7 @@ arr: []; Array comprehensions are a way to create an array based on another iterable. Uses may include performing an operation on each item of the iterable, or creating a subsequence of those items that satisfy a certain condition. -They are written similarly to foreach loops; they can come in two forms, as follows: +They are written similarly to `foreach` loops; they can come in two forms, as follows: ```sm [expression ... member ->? iterable] @@ -53,11 +53,13 @@ Here are two equivalent approaches: ```sm input: [/, /\, //, /\\, /\/]; +== Approach 1 arr: []; ... n ->? input { arr+: [n ++ n]; } +== Approach 2 arr: [n ++ n ... n ->? input]; ``` @@ -69,6 +71,7 @@ There are again two equivalent approaches: ```sm arr: [/, /\\, /\\/, /\\\\, //\\/]; +== Approach 1 filtered: []; ... n ->? arr { ? n --- /\ :: / { @@ -76,6 +79,7 @@ filtered: []; } } +== Approach 2 filtered: [n ... n ->? arr ? n --- /\ :: /]; ``` @@ -94,11 +98,13 @@ Table comprehensions have a similar syntax to array comprehensions: For example, both of the following approaches are equivalent: ```sm +== Approach 1 tab: {{}}; ... x ->? [/\, /\\, //\] { tab<>: x ++ x; } +== Approach 2 tab: {{x -> x ++ x ... x ->? [/\, /\\, //\]}}; ``` @@ -124,7 +130,7 @@ x: \; ## `break`/`continue` `break` statements are written with `<-`, and terminate the enclosing loop immediately. -They can be used in both `for` and `while` loops. +They can be used in both `foreach` and `while` loops. ```sm x: \; diff --git a/docs/enums.md b/docs/enums.md index 542e1543..362546c7 100644 --- a/docs/enums.md +++ b/docs/enums.md @@ -8,7 +8,7 @@ By default, enum members are assigned increasing numbers, starting from 0. You can provide your own values for enum members by simply assigning values to the names. -Enum members cannot be modified. +Enum members cannot be modified. Enums are truthy when they have at least 1 member. ```sm Shape # { @@ -34,4 +34,27 @@ Enum(Shape) 0 1 #FF0000 +``` + +Enums can be casted to Tables: +```sm +Shape # { + Circle; + Square; +} + +Color # { + Red: "#FF0000"; + Green: "#00FF00"; + Blue: "#0000FF"; +} + +=> * { + Shape%!; + Color%!; +} +``` +``` +{{"Circle" -> 0, "Square" -> 1}} +{{"Red" -> "#FF0000", "Green" -> "#00FF00", "Blue" -> "#0000FF"}} ``` \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index eab35f20..00ab8094 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -29,9 +29,8 @@ ```sm dropsort array * { - <-iter.enumerate; out: []; - ... i, v ->? enumerate(array) { + ... i, v ->? <<>> >< array { ? ~~ out || v >: out<<-/>> { out+: [v]; } @@ -58,9 +57,7 @@ factorial n * { ```sm point coords... * { - <-string.join; - <-types.String; - * "(" + join(coords, ", ") + ") is a " + String(coords$) + "D point"; + * "(" + <-string.join(coords, ", ") + ") is a " + ""?!(coords$) + "D point"; } => * { @@ -77,8 +74,7 @@ point coords... * { ```sm => * { - <-iter.range; - nums: range(/, /\//)!; + nums: <>!; ... n ->? nums { ? n --- /\ :: \ { -> } ? n :: /\\/ { <- } @@ -100,18 +96,14 @@ point coords... * { ```sm random_hex_color * { - <-iter.range; - <-string.hexdigits, join; - * "#" + join([hexdigits?? ... i ->? range(/\\)]); + * "#" + <-string.join([<-string.hexdigits?? ... i ->? <<../\\>>]); } == could alternatively define that as random_hex_color * { max: /\ +++ //\\\; - <-string.leftpad; - <-math.to_hex; - * "#" + leftpad(to_hex(max??), //\, "0"); + * "#" + <-string.leftpad(<-math.to_hex(max??), //\, "0"); } => * { @@ -131,8 +123,7 @@ roll_dice q? * { ? q :: / { * roll1(); } ,, { - <-iter.range; - * [roll1() ... i ->? range(q)]; + * [roll1() ... i ->? <<..q>>]; } } @@ -154,8 +145,7 @@ roll_dice q? * { } to_string * { - <-types.String; - * "Rectangle[" + String('a) + ", " + String('b) + "]"; + * "Rectangle[" + ""?!('a) + ", " + ""?!('b) + "]"; } circumference * { diff --git a/docs/fileio.md b/docs/fileio.md index 677e869b..3ec20f5f 100644 --- a/docs/fileio.md +++ b/docs/fileio.md @@ -2,6 +2,7 @@ Files are handled through file I/O objects, which can be in one of several modes: read, write, read & write, append, and as either text or binary for each of these. File I/O objects have a cursor, which is updated whenever data is written to/read from the object. +File objects are truthy when they are open. The current cursor position can be gotten like so: ```sm @@ -149,8 +150,7 @@ Integer value | Name An example use of these could be printing without a newline at the end: ```sm => * { - <-iter.range; - ... i ->? range(/\/\) { + ... i ->? <<../\/\>> { i ~> /; } } diff --git a/docs/functions.md b/docs/functions.md index 5489d1b8..52a36c68 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -108,7 +108,7 @@ code_to_char(/\\\\/)!; == !! Functions can yield values instead of returning them, thus making the function behave like an iterator. Values are yielded with the `**` operator: ```sm -<-math.is_prime; +<=math.is_prime; prime_generator * { x: /\; @@ -143,4 +143,6 @@ Iterators support special `$` and cast `%` methods. `Iterator%` returns the length of the iterator if available, and null otherwise. -`Iterator$` yields the next value of the iterator. \ No newline at end of file +`Iterator$` yields the next value of the iterator. + +Iterators are always truthy. diff --git a/docs/index.md b/docs/index.md index 1ba100d5..32fe583b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,12 +17,17 @@ Here's a `Hello, World!` program written in Samarium: ## [pip](https://pypi.org/project/pip/) -`pip install samarium` +```sh +pip install samarium +``` ## [AUR](https://aur.archlinux.org/) -`git clone https://aur.archlinux.org/samarium.git; cd samarium; makepkg -sirc` or use your favorite [AUR helper](https://wiki.archlinux.org/title/AUR_helpers). +```sh +git clone https://aur.archlinux.org/samarium.git && cd samarium && makepkg -sirc +``` +or use your favorite [AUR helper](https://wiki.archlinux.org/title/AUR_helpers). ## Using Samarium @@ -52,6 +57,6 @@ Special thanks to: - [tetraxile](https://github.com/tetraxile) for helping with design choices and writing the docs - [MithicSpirit](https://github.com/MithicSpirit) for making an AUR package for Samarium - [DarviL82](https://github.com/DarviL82) for fixing some issues -- [Endercheif](https://github.com/Endercheif) for making the documentation look fancy and helping with design choices +- [Endercheif](https://github.com/Endercheif) for making the documentation look fancy, helping with design choices, and adding partial Python Interoperability If you have any questions, or would like to get in touch, join the [Discord server](https://discord.gg/C8QE5tVQEq)! diff --git a/docs/integers.md b/docs/integers.md index 56db97cc..dad2b3e8 100644 --- a/docs/integers.md +++ b/docs/integers.md @@ -31,3 +31,9 @@ Or in base 10: ```py 99999999999999999999999999999999999999999999999999999999999999999999999999999999 ``` + +Integers can be casted to characters represented by that integer's unicode code point: + +`/\\\\/%` returns `"!"`. + +`//////%` returns `"?"`. \ No newline at end of file diff --git a/docs/interop.md b/docs/interop.md new file mode 100644 index 00000000..e8f5c2eb --- /dev/null +++ b/docs/interop.md @@ -0,0 +1,93 @@ +# Python Interoperability + +Samarium 0.4.0 introduced partial Python Interoperability, allowing you to use +Python functions inside Samarium. + +The Python file you want to use has to be in the same directory as your Samarium +file (so standard importing rules apply). + +Making a Python function usable in Samarium is as easy as decorating it with +`@export`β€”it's gonna do all conversions between supported Samarium and Python +types automatically. + +Python files are imported the same way as Samarium files. + +Samarium files take priority over Python files, meaning that if you have both +`file.py` and `file.sm` in the same folder, Samarium will import `file.sm`. + +## Examples + +### Example 1 +```py +# foo.py +from samarium.python import export + +@export +def sqrt(n: int) -> str: + return str(n ** 0.5) +``` +```sm +=> * { + <-foo.sqrt(/\)!; == 1.4142135623730951 +} +``` + +### Example 2 +```py +# bar.py +from samarium.python import export, SliceRange + +@export +def find_perfect_squares(sr: SliceRange) -> list[int]: + return [n for n in sr.range if (n ** 0.5).is_integer()] +``` +```sm +=> * { + <-bar.find_perfect_squares(<<../\/\/\/>>)!; + == [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] +} +``` + +## Supported Conversions + +### Samarium β†’ Python +Samarium Type | Python Type +--- | --- +Array | list +Enum | Enum +File | IOBase +Integer | int +Iterator | Iterator +Null | NoneType +Slice | SliceRange[^1] +String | str +Table | dict +Zip | zip + +[^1]: + +### Python β†’ Samarium +Python Type | Samarium Type +--- | --- +int | Integer +bool | Integer +float | Integer +str | String +NoneType | Null +list | Array +tuple | Array +set | Array +dict | Table +range | Slice +slice | Slice +SliceRange[^1] | Slice +IOBase | File +zip | Zip +type[Enum] | Enum +Iterator | Iterator + +Additionally, Enum members only get their values converted. + +[^1]: Samarium Slices can act as both Python `range`s and `slice`s, therefore a +`SliceRange` object is being returned. It only has 2 properties, `range` and +`slice`, which return exactly those objects. \ No newline at end of file diff --git a/docs/modules.md b/docs/modules.md index 6551fc66..6e6a1e3f 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -9,9 +9,10 @@ Like variables, module names must consist of only letters and numbers, and are c Modules can be imported using the `<-` operator, followed by the module's name. Objects (classes, functions, variables) from this module can then be accessed with the `.` operator. +Module objects are always truthy. ```sm -<-string; +<=string; == imports the `string` module from Samarium's standard library string.to_upper("abc")!; @@ -24,17 +25,52 @@ string.digits!; Objects can also be directly imported from a module one by one, in which case they don't need to be preceded by the module name when using them: ```sm -<-math.abs, sqrt; +<=types.UUID4; +<=math.[abs, sqrt]; -sqrt(/\\/)!; == prints 3 -abs(-/\)!; == prints 2 +UUID4()!; == a3ace080-8545-4852-a4d8-0385ccbb6a70 +sqrt(/\\/)!; == 3 +abs(-/\)!; == 2 ``` All objects in a module can be directly imported at once by using the wildcard character `*`. Importing everything in this way is typically advised against, as it may cause poorly readable code and/or name collisions. ```sm -<-math.*; +<=math.*; factorial(//)!; == prints 6 ``` + + +### Import Aliases + +Imported objects can be renamed if needed by using the `name -> new_name` syntax: +```sm +<=string.[to_upper -> shout, strip]; + +str: " hello! "; +shout(strip(str))!; == HELLO! +``` +This is different to +``` +<=string.to_upper; + +shout: to_upper; +``` +because in this case, both `to_upper` and `shout` are valid options, +whereas the first code block only has `shout`. + + +### Inline Imports + +Imports that are going to be only used once can be replaced +by inline imports (using the `<-` keyword): +```sm +=> * { + i: /?!("Enter a number: "???); + ? <-math.is_prime(i) { + i, "is a prime number"!; + } +} +``` \ No newline at end of file diff --git a/docs/stdcollections.md b/docs/stdcollections.md index 4ddcd21d..6d63035c 100644 --- a/docs/stdcollections.md +++ b/docs/stdcollections.md @@ -11,16 +11,16 @@ A stack is a collection of items that the user may "push" a new item on top of, Method | Use --- | --- -`create([size])` | Initializes an empty Stack object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the stack unbounded capacity. +`=>([size])` | Initializes an empty Stack object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the stack unbounded capacity. `is_empty()` | Returns `1` if the number of items in the stack is equal to 0, otherwise returns `0`. `is_full()` | Returns `1` if the number of items in the stack is equal to the specified capacity,
otherwise returns `0`.[^1] `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. `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. `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. `push_all(items)` | Pushes each element of `items` on top of the stack, one at a time. -`special()` | Returns the number of items in the stack. -`to_bit()` | Returns `1` if the stack is not empty, otherwise returns `0`.[^2] -`to_string()` | Returns some information about the stack as a string;
its capacity, number of items, and the value of the top item. +`$` | Returns the number of items in the stack. +`?` | Returns `1` if the stack is not empty, otherwise returns `0`.[^2] +`!` | Returns some information about the stack as a string;
its capacity, number of items, and the value of the top item. @@ -33,7 +33,7 @@ A queue is a collection of items that the user may "put" ("enqueue") an item at Method | Use --- | --- -`create([size])` | Initializes an empty Queue object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the queue unbounded capacity. +`=>([size])` | Initializes an empty Queue object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the queue unbounded capacity. `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. `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. `is_empty()` | Returns `1` if the number of items in the queue is equal to 0, otherwise returns `0`. @@ -41,9 +41,9 @@ Method | Use `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. `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. `put_all(items)` | Puts each element of `items` at the back of the queue, one at a time. -`special()` | Returns the number of items in the queue. -`to_bit()` | Returns `1` if the queue is not empty, otherwise returns `0`.[^2] -`to_string()` | 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. +`$` | Returns the number of items in the queue. +`?` | Returns `1` if the queue is not empty, otherwise returns `0`.[^2] +`!` | 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. @@ -56,8 +56,8 @@ A deque is a data structure similar to a queue, but where insertion and removal Method | Use --- | --- +`=>([size])` | Initializes an empty Deque object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the deque unbounded capacity. `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. -`create([size])` | Initializes an empty Deque object with capacity `size`.
If `size` is unspecified it will default to `-1`, giving the deque unbounded capacity. `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. `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. `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. @@ -67,9 +67,9 @@ Method | Use `put_all(items)` | Puts each element of `items` at the back of the deque, one at a time. `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. `put_front_all(items)` | Puts each element of `items` at the front of the deque, one at a time. -`special()` | Returns the number of items in the deque. -`to_bit()` | Returns `1` if the deque is not empty, otherwise returns `0`.[^2] -`to_string()` | 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. +`$` | Returns the number of items in the deque. +`?` | Returns `1` if the deque is not empty, otherwise returns `0`.[^2] +`!` | 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. @@ -82,21 +82,26 @@ A set is an unordered collection of items, with no duplicates. Method | Use --- | --- +`=>([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. +`items` | The contents of the set as an array. `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. `clear()` | Removes every element from the set. -`create([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. -`difference(other)` | Returns the difference of the current set and `other`,
i.e. a new set containing all items that the current set contains
but `other` does not. -`has(value)` | Returns `1` if `value` is contained in the set, otherwise returns `0`. -`intersection(other)` | Returns the intersection of the current set and `other`,
i.e. a new set containing all items that the two sets share. +`value ->?` | Returns `1` if `value` is contained in the set, otherwise returns `0`. `is_empty()` | Returns `1` if the number of items in the set is equal to 0,
otherwise returns `0`. `is_full()` | Returns `1` if the number of items in the set
is equal to the specified capacity, otherwise returns `0`.[^1] -`is_subset(other)` | Returns `1` if the current set is a subset of `other`,
i.e. every element of the current set is contained in `other`. `remove(value)` | Removes `value` from the set, provided it exists in the set. -`special()` | Returns the number of items in the set. -`to_bit()` | Returns `1` if the deque is not empty, otherwise returns `0`.[^2] -`to_string()` | Returns some information about the set as a string;
its capacity, number of items, and the values of its items. -`union(other)` | Returns the union of the current set and `other`,
i.e. the two sets' items added together with duplicates again removed. -`values()` | Returns the contents of the set as an array. +`- other` | Returns the difference of the current set and `other`. +`& other` | Returns the intersection of the current set and `other`. +`:: other` | Returns `1` if the current set and `other` have the same elements, `0` otherwise. +`::: other` | Returns `1` if the current set and `other`
don't have the same elements, `0` otherwise. +`<: other` | Returns `1` if the current set is a subset of `other`. +`< other` | Returns `1` if the current set is a strict subset of `other`. +`>: other` | Returns `1` if the current set is a superset of `other`. +`> other` | Returns `1` if the current set is a strict superset of `other`. +`| other` | Returns the union of the current set and `other`. +`$` | Returns the number of items in the set. +`?` | Returns `1` if the deque is not empty, otherwise returns `0`.[^2] +`!` | Returns some information about the set as a string;
its capacity, number of items, and the values of its items. @@ -109,12 +114,12 @@ A static array is like a normal array, but with a fixed size. They may also enfo Method | Use --- | --- -`create(value[, type])` | Initializes a `StaticArray` object. If `type` is specified,
it defines the enforced type of the static array.
If `value` is an integer, it defines the size of the static array.
If `value` is an array, its size defines the size of the static array,
and its elements are copied into the static array.
If `type` is not specified, the static array will not enforce elements
to be of any particular type. -`get_item(index)` | Returns the `index`th item in the static array.
If `index` is outside the bounds of the static array, an error is thrown. -`set_item(index, value)` | Sets the `index`th item in the static array to `value`.
If `index` is outside the bounds of the static array,
or `value` is not of the correct type for the array,
provided it enforces a particular type, an error is thrown. -`special()` | Returns an array containing all items in the static array,
with any unassigned indices ignored. -`to_bit()` | Returns `1` if the static array is not empty, otherwise returns `0`. -`to_string()` | Returns some information about the static array as a string;
its size, its type ("null" if it doesn't enforce one),
and a table mapping each item's index to its value. +`=>(value[, type])` | Initializes a `StaticArray` object. If `type` is specified,
it defines the enforced type of the static array.
If `value` is an integer, it defines the size of the static array.
If `value` is an array, its size defines the size of the static array,
and its elements are copied into the static array.
If `type` is not specified, the static array will not enforce elements
to be of any particular type. +`<>` | Returns the `index`th item in the static array.
If `index` is outside the bounds of the static array, an error is thrown. +`<>: value` | Sets the `index`th item in the static array to `value`.
If `index` is outside the bounds of the static array,
or `value` is not of the correct type for the array,
provided it enforces a particular type, an error is thrown. +`$` | Returns an array containing all items in the static array,
with any unassigned indices ignored. +`?` | Returns `1` if the static array is not empty, otherwise returns `0`. +`!` | Returns some information about the static array as a string;
its size, its type ("null" if it doesn't enforce one),
and a table mapping each item's index to its value. @@ -124,7 +129,7 @@ Method | Use An arithmetic array is an array which can be used with different binary operators. ```sm -<-collections.ArithmeticArray; +<=collections.ArithmeticArray; aa: ArithmeticArray([/\, //, /\/]); @@ -149,12 +154,11 @@ ArithmeticArray allows item assignment and inherits behavior for `$`, `to_bit`, You can also use your own custom operators in the form of functions by using the `apply(op, other)` method: ```sm -<-collections.ArithmeticArray; -<-math.shl; +<=collections.ArithmeticArray; aa: ArithmeticArray([///, /\//, //\/]); aa!; -aa.apply(shl, /\)!; +aa.apply(<-math.shl, /\)!; remove_null nullable default * { diff --git a/docs/stddatetime.md b/docs/stddatetime.md index d8f8894b..ed448e5a 100644 --- a/docs/stddatetime.md +++ b/docs/stddatetime.md @@ -13,7 +13,7 @@ Function | Use --- | --- `sleep_seconds(seconds)` | Pauses execution for `seconds` seconds. `sleep_minutes(minutes)` | Pauses execution for `minutes` minutes. -`is_leap_year(year)` | Returns `1` if the year is a leap year otherwise `0`. +`is_leap_year(year)` | Returns `1` if the year is a leap year, `0` otherwise. `days_in_month(year, month)`| Returns the amount of days in a given month keeping in track leapyears. `month_name(n)` | Gives the name of a numbered month where `1` is `January`. `weekday_name(n)` | Gives the day of a week from a number
where `1` is a `Monday` and `7` is `Sunday`. @@ -28,10 +28,8 @@ Function | Use Method | Use --- | --- `subtract(other)` | Subtract two DateTime objects, returns a DTDiff object. -`to_string` | Return the date and time in the format `Y-M-D h:m:s.z`. -`to_timestamp` | Returns the time in Unix Time milliseconds. +`to_timestamp()` | Returns the date & time as a Unix timestamp in milliseconds. +`from_timestamp()` (static) | Returns a DateTime object from a Unix timestamp in milliseconds. +`!` | Return the date and time in the format `Y-M-D h:m:s.z`. - - -[^1]: [Special Methods](/classes) diff --git a/docs/stdio.md b/docs/stdio.md new file mode 100644 index 00000000..9918bf39 --- /dev/null +++ b/docs/stdio.md @@ -0,0 +1,82 @@ +# `io` module + +The `io` module contains a few utilities for working with I/O. + +### `io.Bytes` +Allows for easier working with files in binary mode: +```sm +<=io.Bytes; + +=> * { + b: Bytes("ball"); + b!; == 62 61 6c 6c + b+: "!"; == supports Strings, Integers, Arrays of Integers, and other Bytes objects + b.export_string()!; == ball! + b.export()!; == [98, 97, 108, 108, 33] +} +``` + +### `io.inputcast([prompt])` +Works just like `???`, but tries converting the input to a specific type. + +Example (showing all available conversions): +```sm +<=io.inputcast; + +=> * { + .. { + value: inputcast(">> "); + "Type:", value?!!; + "Value:", value!; + } +} +``` +``` +>> hello +Type: String +Value: hello +>> hello there +Type: Array +Value: ["hello", "there"] +>> 1 +Type: Integer +Value: 1 +>> 1 2 +Type: Array +Value: [1, 2] +>> 1, 2 +Type: Array +Value: [1, 2] +>> +Type: Null +Value: null +>> a=1, b=2 +Type: Table +Value: {{"a" -> 1, "b" -> 2}} +>> 3..7 +Type: Slice +Value: <<3..7>> +``` + +### `io.read_until([target])` +Keeps reading lines until the `target` (`""` by default) is entered. +The `target` is included in the output string. + +Example: +```sm +=> * { + "Enter your program:"!; + program: <-io.read_until("}"); + name: "Enter file name: "???; + program ~> name; + "Your program was saved to " + name!; +} +``` +``` +Enter your program: +=> * { + "Hello, World!"!; +} +Enter file name: ball.txt +Your program was saved to ball.txt +``` \ No newline at end of file diff --git a/docs/stditer.md b/docs/stditer.md index 31cfe4d5..05c82675 100644 --- a/docs/stditer.md +++ b/docs/stditer.md @@ -10,29 +10,22 @@ Function | Use `all(array)` | Returns `1` if all elements of `array` are truthy,
`0` otherwise. Returns `1` for empty arrays. `any(array)` | Returns `1` if any of the elements of `array` is truthy,
`0` otherwise. Returns `0` for empty arrays. `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. +`cycle(iter)` | Copies an iterable by consuming it,
and yields its elements in an infinite cycle. `count(array, target)` | Returns the number of times `target` appears in `array`. `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. -`enumerate(array)` | Yields elements of `array` but with each item as a length 2 array
containing the item's index paired with the original item. -`filter(array, function)` | Evaluates `function`[^1] on each item of `array`,
and yields those items that cause `function` to return a truthy value. -`filter_false(array, function)` | Evaluates `function`[^1] on each item of `array`,
and yields those items that cause `function` to return a falsy value. +`filter(function, array)` | Evaluates `function`[^1] on each item of `array`,
and yields those items that cause `function` to return a truthy value. +`filter_false(function, array)` | Evaluates `function`[^1] on each item of `array`,
and yields those items that cause `function` to return a falsy value. `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. `find_all(array, target)` | Finds all instances of `target` in `array`, and yields their indices. `flatten(array[, depth])` | Flattens `array` `depth` times.
By default, flattens recursively as deep as possible. -`map(array, function)` | Applies `function`[^1] to each item of `array`, and yields those new values. +`map(function, array)` | Applies `function`[^1] to each item of `array`, and yields those new values. `pairwise(array)` | Yields successive overlapping pairs taken from `array`. `reduce(array, function)` | 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)]<<-/>>`. `reverse(array)` | Yields the items of `array` in reverse order. `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. `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. -`zip(arrays)` | Iterates over several iterables in parallel,
producing arrays with an item from each one. `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. - - [^1]: Note that `function` must take only one argument (excluding optional parameters). diff --git a/docs/stdmath.md b/docs/stdmath.md index f3b5fad7..a3529fe9 100644 --- a/docs/stdmath.md +++ b/docs/stdmath.md @@ -17,6 +17,6 @@ Function | Use `shl(a, b)` | Returns `a` shifted to the left by `b` bits. `shr(a, b)` | Returns `a` shifted to the right by `b` bits. `sqrt(n)` | Returns the integer square root of the nonnegative integer `n`.
This is the floor of the exact square root of `n`. -`sum(array)` | Sums the items of `array` from left to right and returns the total.
The `array`'s items must be integers. +`sum(array[, start])` | Sums `start` and the items of `array` from left to right
and returns the total. `start` defaults to `0`. diff --git a/docs/stdoperator.md b/docs/stdoperator.md index 371973cd..933beced 100644 --- a/docs/stdoperator.md +++ b/docs/stdoperator.md @@ -4,30 +4,30 @@ This module contains a set of functions corresponding to the native operators of
-Function | Operator ---- | :---: -`add(x, y)` | `x + y` -`and(x, y)` | `x & y` -`cast(x)` | `x%` -`divide(x, y)` | `x -- y` -`equals(x, y)` | `x :: y` -`greater_than(x, y)` | `x > y` -`greater_than_or_equal(x, y)` | `x >: y` -`has(x, y)` | `y ->? x` -`hash(x)` | `x##` -`less_than(x, y)` | `x < y` -`less_than_or_equal(x, y)` | `x <: y` -`mod(x, y)` | `x --- y` -`multiply(x, y)` | `x ++ y` -`not(x)` | `~x` -`not_equals(x, y)` | `x ::: y` -`or(x, y)` | `x | y` -`power(x, y)` | `x +++ y` -`random(x)` | `x??` -`special(x)` | `x$` -`subtract(x, y)` | `x - y` -`to_bit(x)` | `x.to_bit()` -`to_string(x)` | `x.to_string()` -`xor(x, y)` | `x ^ y` +Function | Operator +--- | :---: +`add(x, y)` | `x + y` +`and(x, y)` | `x & y` +`cast(x)` | `x%` +`div(x, y)` | `x -- y` +`eq(x, y)` | `x :: y` +`ge(x, y)` | `x >: y` +`gt(x, y)` | `x > y` +`has(x, y)` | `y ->? x` +`hash(x)` | `x##` +`le(x, y)` | `x <: y` +`lt(x, y)` | `x < y` +`mod(x, y)` | `x --- y` +`mul(x, y)` | `x ++ y` +`not(x)` | `~x` +`ne(x, y)` | `x ::: y` +`or(x, y)` | `x | y` +`pow(x, y)` | `x +++ y` +`random(x)` | `x??` +`special(x)` | `x$` +`sub(x, y)` | `x - y` +`to_bit(x)` | `/ ? x ,, \` +`to_string(x)` | `""?!(x)` +`xor(x, y)` | `x ^ y`
diff --git a/docs/stdstring.md b/docs/stdstring.md index 75ad6058..545a8034 100644 --- a/docs/stdstring.md +++ b/docs/stdstring.md @@ -21,7 +21,6 @@ Function | Use `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. `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`. `ends_with(string, suffix)` | Returns `1` if `string` ends with the substring `suffix`,
otherwise returns `0`. -`format(string, fields)` | Replaces field placeholders with supplied values, e.g.:
`format("Hi $name!", {{"name" -> "Bob"}}) :: "Hi Bob!"`
`format("$$age = $age", {{"age" -> /\\}}) :: "$age = 4"`
When provided with an Array instead of a Table,
the indeces of specific values serve as keys:
`format("{$0@$1}", ["host", "home"]) :: "{host@home}"` `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`. `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`. `is_capitalized(string)` | Returns `1` if `string` is capitalized,
i.e. matches the output of `capitalize(string)` exactly. @@ -35,6 +34,7 @@ Function | Use `is_wrapped(string, chars)` | Returns `1` if `string` both starts and ends with
the substring `chars`, otherwise returns `0`. `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 `" "`. `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. +`ordinal(n)` | Returns an ordinal numeral of a number,
e.g. `ordinal(/)` returns `"1st"`. `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. `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. `split(string[, delimiter])` | Returns an array of the words in `string`,
separated by `delimiter`.
If `delimiter` is not specified, it defaults to `" "`. diff --git a/docs/stdtypes.md b/docs/stdtypes.md index 7a7fc829..e1685ed4 100644 --- a/docs/stdtypes.md +++ b/docs/stdtypes.md @@ -19,7 +19,7 @@ Booleans support several operations: all comparison operations, all arithmetic o Generates a random UUID, version 4. Returns a UUID object with attributes `hex` and `dec`: ```sm -<-types.UUID4; +<=types.UUID4; uuid: UUID4(); uuid!; @@ -76,7 +76,7 @@ Integers can be constructed from strings, including binary, octal, and hexadecim ### Null -The `Null` type alias is defined to be equal to `[,]<<\>>?!`. It can be used to explicitly use null values instead of relying on implicit null triggers. +The `Null` type alias is defined to be equal to `(||)?!`. It can be used to explicitly use null values instead of relying on implicit null triggers. ### Slice @@ -110,3 +110,12 @@ Calling `Table()` with no arguments will return an empty table. Tables can be constructed from arrays containing 2-element iterables:
`Table([[//, /\\/], "XD"])` is equivalent to `{{// -> /\\/, "X" -> "D"}}` + +### Zip + +The `Zip` type alias is defined to be equal to `("" >< "")?!`. + +Calling `Zip()` with no arguments will return an empty iterator. +Passing in only 1 argument is equivalent to `... i ->? iter { ** [i]; }`. +Calling `Zip()` with 2 or more arguments is equivalent to using the `><` +operator on them. \ No newline at end of file diff --git a/docs/strings.md b/docs/strings.md index ca7493b4..c3d3d14e 100644 --- a/docs/strings.md +++ b/docs/strings.md @@ -15,8 +15,44 @@ multiline string" ``` + +## Basic Operations + Strings can be manipulated using some arithmetic operators: `"hello" + "world"` is the same as `"helloworld"` `"hello" ++ //` is the same as `"hellohellohello"` + +`"hello" - "l"` is the same as `"heo"` (the 2nd operand removes all instances +of itself from the 1st operand) + + +## Formatting + +Strings can be formatted using the `---` operator. The 2nd operand can be a String, Array, or a Table. +```sm +"Hi $0!" --- "Bob"!; +== Hi Bob! + +"$0$1$0" --- ["abra", "cad"]!; +abracadabra + +s: "Coordinates: $lat, $long"; +coords: {{ + "lat" -> "56.37N", + "long" -> "-8.34W" +}}; +s --- coords!; +== Coordinates: 56.37N, -8.34W +``` + + +## Casting + +Strings can be casted to Integers (for single characters) or Arrays of Integers +(for longer strings), representing the Unicode code point of each character: + +`"a"%` returns `97`. + +`"hi!"%` returns `[104, 105, 33]`. \ No newline at end of file diff --git a/docs/tools.md b/docs/tools.md index 6a3cf462..6c2f1c0e 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -19,29 +19,29 @@ $ chmod +x script If you run the `samarium` command without any other arguments, you'll launch the REPL, an interactive shell that will read and evaluate any Samarium code you enter. ```txt $ samarium -Samarium 0.3.1 -==> +Samarium 0.4.0 +--> ``` Interacting with the REPL is a nice way to experiment with Samarium: ```txt -==> / + /\! +--> / + /\! 3 -==> 1: "ball"##! +--> 1: "ball"##! 1083481267058749873 -==> 1??! +--> 1??! 443527852557841359 -==> 1??! +--> 1??! 894622914084910886 ``` The REPL also supports compound statements: ```txt -==> x: /\/\?? -==> ? x --- /\ :: \ { +--> x: /\/\?? +--> ? x --- /\ :: \ { > x -- /\!; > } ,, { > 3 ++ x + /!; > } 4 -==> x! +--> x! 8 ``` diff --git a/docs/zip.md b/docs/zip.md new file mode 100644 index 00000000..88bd9dbe --- /dev/null +++ b/docs/zip.md @@ -0,0 +1,46 @@ +# Zipping + +Samarium's zip operator `><` allow for easy zipping, that is, iterating through multiple iterables at once: +```sm +names: ["Alice", "Bob", "Charlie"]; +ages: [/\\\\, /\\\/, ////]; + +... i ->? names >< ages { + "$0 is $1 years old" --- i!; +} +``` +``` +Alice is 16 years old +Bob is 17 years old +Charlie is 15 years old +``` + +Enumerating iterables can be simulated by using an empty slice object: +```sm +x: ["Alpha", "Beta", "Gamma"]; + +... i, v ->? <<>> >< x { + "x<<$0>> :: $1" --- [i, v]!; +} +``` +``` +x<<0>> :: Alpha +x<<1>> :: Beta +x<<2>> :: Gamma +``` +
+ +```sm +<=string.ordinal; + +winners: ["Jake", "Clarisse", "Matt"]; + +... p, w ->? <> >< winners { + "$0 place: $1" --- [ordinal(p), w]; +} +``` +``` +1st place: Jake +2nd place: Clarisse +3rd place: Matt +``` diff --git a/mkdocs.yml b/mkdocs.yml index b5fe9b92..282b358f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,14 +17,17 @@ nav: - Control Flow: controlflow.md - Null: null.md - Slices: slices.md + - Zipping: zip.md - Enums: enums.md - Functions: functions.md - Modules: modules.md - Classes: classes.md - File I/O: fileio.md + - Python Interop: interop.md - Standard Library: - collections module: stdcollections.md - datetime module: stddatetime.md + - io module: stdio.md - iter module: stditer.md - math module: stdmath.md - operator module: stdoperator.md diff --git a/poetry.lock b/poetry.lock index 5a446b6d..a748b4f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,70 +1,147 @@ [[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" +name = "black" +version = "22.10.0" +description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "crossandra" +version = "1.2.2" +description = "A simple tokenizer operating on enums with a decent amount of configuration" +category = "main" +optional = false +python-versions = ">=3.9,<4.0" + +[package.dependencies] +result = ">=0.8.0,<0.9.0" + +[[package]] +name = "dahlia" +version = "2.1.1" +description = "A library allowing you to use Minecraft format codes in strings." +category = "main" +optional = false +python-versions = ">=3.8,<4.0" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "dev" optional = false python-versions = "*" [[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" +name = "pathspec" +version = "0.10.2" +description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" [[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" +name = "platformdirs" +version = "2.5.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.4)", "sphinx (>=5.3)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] [[package]] -name = "termcolor" -version = "1.1.0" -description = "ANSII Color formatting for output in terminal." +name = "result" +version = "0.8.0" +description = "A Rust-like result type for Python" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" [metadata] lock-version = "1.1" -python-versions = ">=3.9" -content-hash = "4f9451d7728e81899f6b9f7fab10e970708e39b0fb45f32790f65d6c0ea9bb2b" +python-versions = "^3.9" +content-hash = "cea9e7489990de7f787ace958a24bdbd319e5fd6052a5481224545139bb37285" [metadata.files] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -termcolor = [ - {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, -] +black = [] +click = [] +colorama = [] +crossandra = [] +dahlia = [] +isort = [] +mypy-extensions = [] +pathspec = [] +platformdirs = [] +result = [] +tomli = [] +typing-extensions = [] diff --git a/pyproject.toml b/pyproject.toml index 6ec73c66..1f553f39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "samarium" -version = "0.3.1" +version = "0.4.0" description = "The Samarium Programming Language" authors = ["trag1c "] license = "MIT" @@ -10,10 +10,12 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.9" -dahlia = "^1.0.0" +dahlia = "^2.1.0" +crossandra = "^1.2.2" [tool.poetry.dev-dependencies] -flake8 = "^4.0.1" +black = "^22.10.0" +isort = "^5.10.1" [tool.poetry.scripts] samarium = "samarium.__main__:main" diff --git a/src/samarium/__init__.py b/src/samarium/__init__.py index c703e712..867ca5b7 100644 --- a/src/samarium/__init__.py +++ b/src/samarium/__init__.py @@ -1,15 +1,13 @@ import sys - from contextlib import suppress +from pathlib import Path -from .core import run, readfile +from .core import run from .shell import run_shell from .transpiler import Registry from .utils import __version__ -MAIN = Registry(globals()) - -OPTIONS = ["-v", "--version", "-c", "--command", "-h", "--help"] +OPTIONS = ("-v", "--version", "-c", "--command", "-h", "--help") HELP = """samarium [option] [-c cmd | file] options and arguments: @@ -19,29 +17,31 @@ file : reads program from script file""" -def main(debug: bool = False): +def main(debug: bool = False) -> None: + + reg = Registry(globals()) if len(sys.argv) == 1: - return run_shell(debug) + return run_shell(debug=debug) if (arg := sys.argv[1]) in OPTIONS: if arg in OPTIONS[:2]: print(f"Samarium {__version__}") elif arg in OPTIONS[2:4]: - run(f"=> argv * {{\n\t{sys.argv[2]} !;\n}}", MAIN, debug) + run(f"=> argv * {{ {sys.argv[2]} !; }}", reg, debug) elif arg in OPTIONS[4:]: print(HELP) sys.exit() try: - file = readfile(arg) + file = Path(arg).read_text() except IOError: - print(f"file not found: {arg}") + print(f"file not found: {arg}", file=sys.stderr) else: with suppress(Exception, KeyboardInterrupt): file = "\n".join(file.splitlines()[file.startswith("#!") :]) - run(file, MAIN, debug) + run(file, reg, debug) -def main_debug(): +def main_debug() -> None: main(debug=True) diff --git a/src/samarium/builtins.py b/src/samarium/builtins.py new file mode 100644 index 00000000..a559c9f5 --- /dev/null +++ b/src/samarium/builtins.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +from contextlib import contextmanager +from datetime import datetime +from functools import wraps +from inspect import signature +from re import compile +from time import sleep as _sleep +from time import time_ns +from types import FunctionType, GeneratorType +from typing import Any, Callable +from typing import Iterator as Iter +from typing import TypeVar + +from .classes import ( + MISSING, + NULL, + Array, + Attrs, + Int, + Integer, + Iterator, + Null, + Slice, + String, + Type, + to_string, +) +from .exceptions import ( + SamariumError, + SamariumIOError, + SamariumSyntaxError, + SamariumTypeError, +) +from .utils import get_name + +MISSING_ARGS_PATTERN = compile( + r"\w+\(\) takes exactly one argument \(0 given\)" + r"|\w+\(\) missing (\d+) required positional argument" +) + +NULL_STRING = String() + +T = TypeVar("T") + +TOO_MANY_ARGS_PATTERN = compile( + r"\w+\(\) takes (\d+) positional arguments? but (\d+) (?:was|were) given" +) + + +def check_type(obj: Any) -> None: + if isinstance(obj, property): + raise SamariumTypeError("cannot use a special method on a type") + if isinstance(obj, (tuple, GeneratorType)): + raise SamariumSyntaxError("invalid syntax") + + +def correct_type(obj: T) -> T | Integer | Iterator | Null: + if obj is None: + return NULL + elif isinstance(obj, bool): + return Int(obj) + elif isinstance(obj, GeneratorType): + return Iterator(obj) + else: + check_type(obj) + return obj + + +def dtnow() -> Array: + utcnow = datetime.utcnow() + now = datetime.now().timetuple() + utcnow_tt = utcnow.timetuple() + tz = now[3] - utcnow_tt[3], now[4] - utcnow_tt[4] + utcnow_tpl = utcnow_tt[:-3] + (utcnow.microsecond // 1000,) + tz + return Array(map(Int, utcnow_tpl)) + + +def function(func: Callable[..., Any]) -> Callable[..., Any]: + @wraps(func) + def wrapper(*args: Any) -> Any: + for arg in args: + check_type(arg) + with modify(func, list(args), argc) as (f, checked_args): + try: + result = correct_type(f(*checked_args)) + except TypeError as e: + errmsg = str(e) + if "positional argument: 'self'" in errmsg: + raise SamariumTypeError("missing instance") from None + missing_args = MISSING_ARGS_PATTERN.search(errmsg) + if missing_args: + given = argc - (int(missing_args.group(1)) or 1) + raise SamariumTypeError( + f"not enough arguments ({given}/{argc})" + ) from None + too_many_args = TOO_MANY_ARGS_PATTERN.search(errmsg) + if too_many_args: + raise SamariumTypeError( + f"too many arguments ({too_many_args.group(2)}/{argc})" + ) from None + raise e + return result + + argc = len(signature(func).parameters) + + wrapper.__str__ = lambda: get_name(func) + wrapper.special = lambda: Int(argc) + wrapper.argc = argc + wrapper.parent = lambda: Type(FunctionType) + wrapper.type = lambda: Type(FunctionType) + wrapper.hash = lambda: Int(hash(func)) + + return wrapper + + +@contextmanager +def modify( + func: Callable[..., Any], args: list[Any], argc: int +) -> Iter[tuple[Callable[..., Any], list[Any]]]: + flag = func.__code__.co_flags + if flag & 4 == 0: + yield func, args + return + x = argc - 1 + args = [*args[:x], Array(args[x:])] + func.__code__ = func.__code__.replace(co_flags=flag - 4, co_argcount=argc) + if not argc: + args = [] + yield func, args + func.__code__ = func.__code__.replace(co_flags=flag, co_argcount=argc - 1) + + +def mkslice(start: Any = None, stop: Any = MISSING, step: Any = None) -> Any: + if stop is MISSING: + if start is not None: + return start + return Slice(NULL) + start = NULL if start is None else start + stop = NULL if stop is None else stop + step = NULL if step is None else step + return Slice(start, stop, step) + + +def print_safe(*args: Attrs | Callable[..., Any] | bool | None) -> Attrs: + typechecked_args = list(map(correct_type, args)) + return_args = typechecked_args.copy() + strs = list(map(to_string, typechecked_args)) + types = list(map(type, typechecked_args)) + if tuple in types: + raise SamariumSyntaxError("missing brackets") + if GeneratorType in types: + raise SamariumSyntaxError("invalid comprehension") + print(*strs) + if len(return_args) > 1: + return Array(return_args) + elif not return_args or types[0] is Null: + return NULL + return return_args[0] + + +def readline(prompt: String = NULL_STRING) -> String: + try: + return String(input(prompt.val)) + except KeyboardInterrupt: + msg = "^C" + except EOFError: + msg = "^D" + raise SamariumIOError(msg) + + +def sleep(*args: Integer) -> None: + if not args: + raise SamariumTypeError("no argument provided for ,.,") + if len(args) > 1: + raise SamariumTypeError(",., only takes one argument") + if not isinstance((time := args[0]), Integer): + raise SamariumTypeError(",., only accepts integers") + _sleep(time.val / 1000) + + +def t(obj: T | None = None) -> T | None: + return obj + + +def throw(message: String = NULL_STRING) -> None: + raise SamariumError(message.val) + + +def timestamp() -> Integer: + return Int(time_ns() // 1_000_000) diff --git a/src/samarium/classes/__init__.py b/src/samarium/classes/__init__.py new file mode 100644 index 00000000..e50c82d6 --- /dev/null +++ b/src/samarium/classes/__init__.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from .base import ( + MISSING, + NEXT, + NULL, + Array, + Attrs, + Enum, + Int, + Integer, + Iterator, + Module, + Null, + Slice, + String, + Table, + Type, + UserAttrs, + Zip, + to_string, +) +from .fileio import File, FileManager, Mode + +__all__ = ( + "Array", + "Attrs", + "Enum", + "File", + "Iterator", + "Slice", + "Table", + "Integer", + "Int", + "String", + "Module", + "Type", + "MISSING", + "NEXT", + "NULL", + "Null", + "FileManager", + "Mode", + "UserAttrs", + "Zip", + "to_string", +) diff --git a/src/samarium/classes/base.py b/src/samarium/classes/base.py new file mode 100644 index 00000000..b62edf02 --- /dev/null +++ b/src/samarium/classes/base.py @@ -0,0 +1,890 @@ +from __future__ import annotations + +from collections.abc import Callable +from functools import lru_cache +from inspect import signature +from secrets import choice, randbelow +from types import FunctionType +from typing import Any, Iterable +from typing import Iterator as Iter + +from ..exceptions import NotDefinedError, SamariumTypeError, SamariumValueError +from ..utils import ( + ClassProperty, + Singleton, + get_name, + get_type_name, + parse_integer, + smformat, +) + + +def functype_repr(obj: Any) -> str: + return ( + get_name(obj) + if isinstance(obj, FunctionType) + or (isinstance(obj, type) and issubclass(obj, UserAttrs)) + else repr(obj) + ) + + +def guard(operator: str, *, default: int | None = None) -> Callable[..., Any]: + def decorator(function: Callable[..., Any]) -> Callable[..., Any]: + def wrapper(self: Any, other: Any) -> Any: + Ts = type(self) + To = type(other) + use_default = default is not None and other.val is None + if isinstance(other, Ts): + return function(self, other) + if use_default: + return function(self, Int(default)) + raise NotDefinedError(f"{Ts.__name__} {operator} {To.__name__}") + + return wrapper + + return decorator + + +def throw_missing(*_) -> None: + raise SamariumTypeError("missing default parameter value(s)") + + +class Missing: + @classmethod + def type(cls) -> None: + throw_missing() + + @classmethod + def parent(cls) -> None: + throw_missing() + + @classmethod + def id(cls) -> None: + throw_missing() + + +METHODS = ( + "getattr add str sub mul floordiv pow mod and or xor neg pos invert getitem " + "setitem eq ne ge gt le lt contains hash call iter" +) + +for m in METHODS.split(): + setattr(Missing, f"__{m}__", throw_missing) + +MISSING = Missing() +NEXT = object() + + +class Attrs: + @classmethod + def id(cls) -> String: + return String(f"{id(cls):x}") + + @classmethod + def type(cls) -> Type: + return Type(cls) + + parent = type + + +class UserAttrs(Attrs): + def __entry__(self, *args: Any) -> None: + ... + + def __init__(self, *args: Any) -> None: + self.__entry__(*args) + + def __string__(self) -> String: + return String(f"<{get_type_name(self)}@{self.id()}>") + + __string__.argc = 1 + + def __str__(self) -> str: + string = self.__string__ + if string.argc != 1: + raise SamariumTypeError( + f"{get_type_name(self)}! should only take one argument" + ) + v = string().val + if isinstance(v, str): + return v + raise SamariumTypeError(f"{get_type_name(self)}! returned a non-string") + + def __bit__(self) -> Integer: + raise NotDefinedError(f"? {get_type_name(self)}") + + __bit__.argc = 1 + + def __bool__(self) -> bool: + bit = self.__bit__ + if bit.argc != 1: + raise SamariumTypeError( + f"? {get_type_name(self)} should only take one argument" + ) + v = bit().val + if v in (0, 1): + return bool(v) + raise SamariumTypeError(f"? {get_type_name(self)} returned a non-bit") + + def __hash__(self) -> int: + return self.hash().val + + def __special__(self) -> Any: + raise NotDefinedError(f"{get_type_name(self)}$") + + __special__.argc = 1 + + def special(self) -> Any: + special = self.__special__ + if special.argc != 1: + raise SamariumTypeError( + f"{get_type_name(self)}$ should only take one argument" + ) + return special() + + def __hsh__(self) -> Integer: + raise NotDefinedError(f"{get_type_name(self)}##") + + __hsh__.argc = 1 + + def hash(self) -> Integer: + hsh = self.__hsh__ + if hsh.argc != 1: + raise SamariumTypeError( + f"{get_type_name(self)}## should only take one argument" + ) + v = hsh() + if isinstance(v, Integer): + return v + raise SamariumTypeError(f"{get_type_name(self)}## returned a non-integer") + + def __cast__(self) -> Any: + raise NotDefinedError(f"{get_type_name(self)}%") + + __cast__.argc = 1 + + def cast(self) -> Any: + cast = self.__cast__ + if cast.argc != 1: + raise SamariumTypeError( + f"{get_type_name(self)}% should only take one argument" + ) + return cast() + + def __random__(self) -> Integer: + raise NotDefinedError(f"{get_type_name(self)}??") + + __random__.argc = 1 + + def random(self) -> Any: + random = self.__random__ + if random.argc != 1: + raise SamariumTypeError( + f"{get_type_name(self)}?? should only take one argument" + ) + return random() + + @ClassProperty + def argc(self) -> int: + return len(signature(self.__init__).parameters) + + @classmethod + def parent(cls) -> Array | Type: + parents = cls.__bases__ + if len(parents) == 1: + parent = parents[0] + if parent is object: + return Type(cls) + return Type(parent) + return Array(map(Type, parents)) + + +class Type(Attrs): + __slots__ = ("val",) + + def __init__(self, type_: type) -> None: + self.val = type_ + + def __eq__(self, other: Any) -> Integer: + if isinstance(other, Type): + return Int(self.val == other.val) + return Int(self.val == other) + + def __ne__(self, other: Any) -> Integer: + if isinstance(other, Type): + return Int(self.val != other.val) + return Int(True) + + def __str__(self) -> str: + if self.val is FunctionType: + return "Function" + return get_name(self.val) + + __repr__ = __str__ + + def __bool__(self) -> bool: + return True + + def __hash__(self) -> int: + return hash(self.val) + + def __call__(self, *args: Any) -> Any: + if self.val is FunctionType: + raise SamariumTypeError("cannot instantiate a function") + if self.val is Module: + raise SamariumTypeError("cannot instantiate a module") + if self.val is Type: + raise SamariumTypeError("cannot instantiate a type") + return self.val(*args) + + +class Module(Attrs): + __slots__ = ("name", "objects") + + def __init__(self, name: str, objects: dict[str, Any]) -> None: + self.name = name + self.objects = objects + + def __bool__(self) -> bool: + return True + + def __str__(self) -> str: + return f"Module[{self.name}]" + + __repr__ = __str__ + + def __getattr__(self, key: str) -> Any: + return self.objects[key] + + +class Integer(Attrs): + __slots__ = ("val",) + + def __init__(self, v: Any = None) -> None: + if isinstance(v, (float, int, bool)): + self.val = int(v) + elif isinstance(v, Integer): + self.val = v.val + elif v is None: + self.val = 0 + elif isinstance(v, String): + self.val = parse_integer(v.val) if v else 0 + else: + raise SamariumTypeError(f"cannot cast {get_type_name(v)} to Integer") + + def __bool__(self) -> bool: + return self.val != 0 + + def __str__(self) -> str: + return str(self.val) + + __repr__ = __str__ + + @guard("+", default=1) + def __add__(self, other: Any) -> Integer: + return Int(self.val + other.val) + + @guard("-", default=1) + def __sub__(self, other: Any) -> Integer: + return Int(self.val - other.val) + + @guard("++", default=2) + def __mul__(self, other: Any) -> Integer: + return Int(self.val * other.val) + + @guard("--", default=2) + def __floordiv__(self, other: Any) -> Integer: + return Int(self.val // other.val) + + @guard("+++", default=2) + def __pow__(self, other: Any) -> Integer: + return Int(self.val ** other.val) + + @guard("---", default=2) + def __mod__(self, other: Any) -> Integer: + return Int(self.val % other.val) + + @guard("&") + def __and__(self, other: Any) -> Integer: + return Int(self.val & other.val) + + @guard("|") + def __or__(self, other: Any) -> Integer: + return Int(self.val | other.val) + + @guard("^") + def __xor__(self, other: Any) -> Integer: + return Int(self.val ^ other.val) + + def __invert__(self) -> Integer: + return Int(~self.val) + + def __pos__(self) -> Integer: + return self + + def __neg__(self) -> Integer: + return Int(-self.val) + + def __eq__(self, other: Any) -> Integer: + if isinstance(other, Integer): + return Int(self.val == other.val) + return Int(False) + + def __ne__(self, other: Any) -> Integer: + if isinstance(other, Integer): + return Int(self.val != other.val) + return Int(True) + + @guard(">", default=0) + def __gt__(self, other: Any) -> Integer: + return Int(self.val > other.val) + + @guard(">:", default=0) + def __ge__(self, other: Any) -> Integer: + return Int(self.val >= other.val) + + @guard("<", default=0) + def __lt__(self, other: Any) -> Integer: + return Int(self.val < other.val) + + @guard("<:", default=0) + def __le__(self, other: Any) -> Integer: + return Int(self.val <= other.val) + + def __hash__(self) -> int: + return self.hash().val + + def cast(self) -> String: + return String(to_chr(self.val)) + + def hash(self) -> Integer: + return Int(hash(self.val)) + + def random(self) -> Integer: + v = self.val + if not v: + return self + elif v > 0: + return Int(randbelow(v)) + return Int(-randbelow(v) - 1) + + def special(self) -> String: + return String(f"{self.val:b}") + + +Int = lru_cache(1024)(Integer) + + +class String(Attrs): + __slots__ = ("val",) + + def __init__(self, value: Any = "") -> None: + self.val = to_string(value) + + def __contains__(self, element: String) -> bool: + return element.val in self.val + + def __iter__(self) -> Iter[String]: + yield from map(String, self.val) + + def __bool__(self) -> bool: + return self.val != "" + + def __str__(self) -> str: + return self.val + + def __repr__(self) -> str: + return f'"{self}"' + + @guard("+") + def __add__(self, other: Any) -> String: + return String(self.val + other.val) + + @guard("-") + def __sub__(self, other: Any) -> String: + return String(self.val.replace(other.val, "")) + + def __mul__(self, other: Any) -> String: + if isinstance(other, Integer): + return String(self.val * other.val) + raise NotDefinedError(f"String ++ {get_type_name(other)}") + + def __mod__(self, other: Any) -> String: + if isinstance(other, (String, Array, Table)): + return String(smformat(self.val, other.val)) + raise NotDefinedError(f"String --- {get_type_name(other)}") + + def __eq__(self, other: Any) -> Integer: + return Int(self.val == other.val) + + def __ne__(self, other: Any) -> Integer: + return Int(self.val != other.val) + + @guard(">") + def __gt__(self, other: Any) -> Integer: + return Int(self.val > other.val) + + @guard(">:") + def __ge__(self, other: Any) -> Integer: + return Int(self.val >= other.val) + + @guard("<") + def __lt__(self, other: Any) -> Integer: + return Int(self.val < other.val) + + @guard("<:") + def __le__(self, other: Any) -> Integer: + return Int(self.val <= other.val) + + def __getitem__(self, index: Integer | Slice) -> String: + if isinstance(index, (Integer, Slice)): + return String(self.val[index.val]) + raise SamariumTypeError(f"invalid index: {index}") + + def __setitem__(self, index: Integer | Slice, value: String) -> None: + if not isinstance(index, (Integer, Slice)): + raise SamariumTypeError(f"invalid index: {index}") + string = [*self.val] + string[index.val] = value.val + self.val = "".join(string) + + def __hash__(self) -> int: + return self.hash().val + + def __matmul__(self, other: Any) -> Zip: + return Zip(self, other) + + def cast(self) -> Array | Integer: + if len(self.val) == 1: + return Int(ord(self.val)) + return Array(Int(ord(i)) for i in self.val) + + def hash(self) -> Integer: + return Int(hash(self.val)) + + def random(self) -> String: + return String(choice(self.val)) + + def special(self) -> Integer: + return Int(len(self.val)) + + +class Array(Attrs): + __slots__ = ("val",) + + def __init__(self, value: Any = None) -> None: + self.val: list[Any] + if value is None: + self.val = [] + elif isinstance(value, Array): + self.val = value.val.copy() + elif isinstance(value, String): + self.val = list(map(String, value.val)) + elif isinstance(value, Table): + self.val = list(map(Array, value.val.items())) + elif isinstance(value, Slice): + if value.stop is NULL: + raise SamariumValueError("cannot convert an infinite Slice to Array") + self.val = list(value.range) + elif isinstance(value, Iterable): + self.val = [*value] + else: + raise SamariumTypeError(f"cannot cast {get_type_name(value)} to Array") + + def __str__(self) -> str: + return "[{}]".format(", ".join(map(functype_repr, self.val))) + + __repr__ = __str__ + + def __bool__(self) -> bool: + return self.val != [] + + def __iter__(self) -> Iter[Any]: + yield from self.val + + def __contains__(self, element: Any) -> bool: + return element in self.val + + def __eq__(self, other: Any) -> Integer: + if isinstance(other, Array): + return Int(self.val == other.val) + return Int(False) + + def __ne__(self, other: Any) -> Integer: + if isinstance(other, Array): + return Int(self.val != other.val) + return Int(True) + + @guard(">") + def __gt__(self, other: Any) -> Integer: + return Int(self.val > other.val) + + @guard(">:") + def __ge__(self, other: Any) -> Integer: + return Int(self.val >= other.val) + + @guard("<") + def __lt__(self, other: Any) -> Integer: + return Int(self.val < other.val) + + @guard("<:") + def __le__(self, other: Any) -> Integer: + return Int(self.val <= other.val) + + def __getitem__(self, index: Any) -> Any: + if isinstance(index, Integer): + return self.val[index.val] + if isinstance(index, Slice): + return Array(self.val[index.val]) + raise SamariumTypeError(f"invalid index: {index}") + + def __setitem__(self, index: Any, value: Any) -> None: + if not isinstance(index, (Integer, Slice)): + raise SamariumTypeError(f"invalid index: {index}") + self.val[index.val] = value + + @guard("+") + def __add__(self, other: Any) -> Array: + return Array(self.val + other.val) + + def __sub__(self, other: Any) -> Any: + new_array = self.val.copy() + if isinstance(other, Array): + for i in other: + new_array.remove(i) + elif isinstance(other, Integer): + new_array.pop(other.val) + elif other is NULL: + return self.val.pop() + else: + raise NotDefinedError(f"Array - {get_type_name(other)}") + return Array(new_array) + + def __mul__(self, other: Any) -> Array: + if isinstance(other, Integer): + return Array(self.val * other.val) + raise NotDefinedError(f"Array ++ {get_type_name(other)}") + + def __matmul__(self, other: Any) -> Zip: + return Zip(self, other) + + def cast(self) -> String: + s = "" + for i in self.val: + if isinstance(i, Integer): + s += to_chr(i.val) + else: + raise SamariumTypeError("array contains non-integers") + return String(s) + + def random(self) -> Any: + if not self.val: + raise SamariumValueError("array is empty") + return choice(self.val) + + def special(self) -> Integer: + return Int(len(self.val)) + + +class Table(Attrs): + __slots__ = ("val",) + + def __init__(self, value: Any = None) -> None: + if value is None: + self.val = {} + elif isinstance(value, Table): + self.val = value.val.copy() + elif isinstance(value, dict): + self.val = value + elif isinstance(value, Array): + arr = value.val + if all(isinstance(i, (String, Array)) and len(i.val) == 2 for i in arr): + table = {} + for e in arr: + if isinstance(e, String): + k, v = map(String, e.val) + table[k] = v + else: + k, v = e.val + table[k] = v + self.val = table + else: + raise SamariumValueError( + "not all elements of the array are of length 2" + ) + else: + raise SamariumTypeError(f"cannot cast {get_type_name(value)} to Table") + + def __str__(self) -> str: + return "{{{{{}}}}}".format( + ", ".join( + f"{functype_repr(k)} -> {functype_repr(v)}" for k, v in self.val.items() + ) + ) + + def __bool__(self) -> bool: + return self.val != {} + + def __getitem__(self, key: Any) -> Any: + try: + return self.val[key] + except KeyError: + raise SamariumValueError(f"key not found: {key}") from None + + def __setitem__(self, key: Any, value: Any) -> None: + self.val[key] = value + + def __iter__(self) -> Iter[Any]: + yield from self.val.keys() + + def __contains__(self, element: Any) -> bool: + return element in self.val + + def __eq__(self, other: Any) -> Integer: + if isinstance(other, Table): + return Int(self.val == other.val) + return Int(False) + + def __ne__(self, other: Any) -> Integer: + if isinstance(other, Table): + return Int(self.val != other.val) + return Int(True) + + @guard("+") + def __add__(self, other: Any) -> Table: + return Table(self.val | other.val) + + def __sub__(self, other: Any) -> Table: + c = self.val.copy() + try: + del c[other] + except KeyError: + raise SamariumValueError(f"key not found: {other}") from None + return Table(c) + + def __matmul__(self, other: Any) -> Zip: + return Zip(self, other) + + def random(self) -> Any: + if not self.val: + raise SamariumValueError("table is empty") + return choice(list(self.val.keys())) + + def special(self) -> Array: + return Array(self.val.values()) + + +class Null(Singleton, Attrs): + __slots__ = ("val",) + + def __init__(self) -> None: + self.val = None + + def __str__(self) -> str: + return "null" + + __repr__ = __str__ + + def __bool__(self) -> bool: + return False + + def __eq__(self, other: Any) -> bool: + return self is other + + def __ne__(self, other: Any) -> bool: + return self is not other + + def __hash__(self) -> int: + return self.hash().val + + def hash(self) -> Integer: + return Int(hash(self.val)) + + +I64_MAX = 9223372036854775807 +NULL = Null() + + +class Slice(Attrs): + __slots__ = ("start", "stop", "step", "tup", "range", "val") + + def __init__(self, start: Any, stop: Any = NULL, step: Any = NULL) -> None: + if step.val == 0: + raise SamariumValueError("step cannot be zero") + self.start = start + self.stop = stop + self.step = step + self.tup = start.val, stop.val, step.val + self.range = range( + self.tup[0] or 0, + I64_MAX if self.tup[1] is None else self.tup[1], + self.tup[2] or 1, + ) + self.val = slice(*self.tup) + + def __bool__(self) -> bool: + return bool(self.range) + + def __iter__(self) -> Iter[Integer]: + yield from map(Int, self.range) + + def __contains__(self, value: Integer) -> bool: + return value.val in self.range + + def __getitem__(self, index: Integer) -> Integer: + return Int(self.range[index.val]) + + def __str__(self) -> str: + start, stop, step = self.tup + if self.is_empty(): + return "<<>>" + string = "" + if start is not None: + string += repr(start) + if stop is step is None: + string += ".." + if stop is not None: + string += f"..{stop!r}" + if step is not None: + if stop is None: + string += ".." + string += f"..{step!r}" + return f"<<{string}>>" + + def __matmul__(self, other: Any) -> Zip: + return Zip(self, other) + + __repr__ = __str__ + + def __eq__(self, other: Any) -> Integer: + if isinstance(other, Slice): + return Int(self.tup == other.tup) + return Int(False) + + def __ne__(self, other: Any) -> Integer: + if isinstance(other, Slice): + return Int(self.tup != other.tup) + return Int(True) + + def random(self) -> Integer: + return Int(choice(self.range)) + + def special(self) -> Integer: + return Int(len(self.range)) + + def is_empty(self) -> bool: + return self.start is self.stop is self.step is NULL + + +class Zip(Attrs): + __slots__ = ("iters", "val") + + def __init__(self, *values: Any) -> None: + if not all(isinstance(i, Iterable) for i in values): + raise SamariumTypeError("cannot zip a non-iterable") + self.iters = values + self.val = zip(*values) + + def __hash__(self) -> int: + return hash(self.val) + + def __bool__(self) -> bool: + return True + + def __iter__(self) -> Iter[Array]: + yield from map(Array, self.val) + + def __matmul__(self, other: Any) -> Zip: + return Zip(*self.iters, other) + + def __str__(self) -> str: + return f"" + + def special(self) -> Integer: + return Int(len(self.iters)) + + +class Iterator(Attrs): + __slots__ = ("val", "length") + + def __init__(self, value: Any) -> None: + if not isinstance(value, Iterable): + raise SamariumTypeError("cannot create an Iterator from a non-iterable") + self.val = iter(value) + try: + self.length = Int(len(value.val)) + except (TypeError, AttributeError): + self.length = NULL + + def __bool__(self) -> bool: + return True + + def __iter__(self) -> Iter[Any]: + yield from self.val + + def __next__(self) -> Any: + return next(self.val) + + def __str__(self) -> str: + return f"" + + def cast(self) -> Integer | Null: + return self.length + + def special(self) -> Any: + return next(self) + + +class Enum(Attrs): + __slots__ = ("name", "members") + + def __bool__(self) -> bool: + return bool(self.members) + + def __init__(self, name: str, **members: Any) -> None: + self.name = name.removeprefix("sm_") + self.members: dict[str, Any] = {} + + i = 0 + for k, v in members.items(): + if v is NEXT: + self.members[k] = Int(i) + i += 1 + else: + self.members[k] = v + if isinstance(v, Integer): + i = v.val + 1 + + def __str__(self) -> str: + return f"Enum({self.name})" + + def __getattr__(self, name: str) -> Any: + try: + return self.members[name] + except KeyError: + raise AttributeError(f"'{self.name}''{name}'") from None + + def __setattr__(self, name: str, value: Any) -> None: + if name.startswith("sm_"): + raise SamariumTypeError("enum members cannot be modified") + object.__setattr__(self, name, value) + + def cast(self) -> Table: + return Table( + { + String(k.removeprefix("__").removeprefix("sm_")): v + for k, v in self.members.items() + } + ) + + +def to_chr(code: int) -> str: + if code >= 0x110000: + raise SamariumValueError("invalid Unicode code point") + return chr(code) + + +def to_string(obj: Attrs | Callable) -> str: + if isinstance(obj, FunctionType): + return ".".join(i.removeprefix("sm_") for i in obj.__qualname__.split(".")) + return str(obj) diff --git a/src/samarium/classes/fileio.py b/src/samarium/classes/fileio.py new file mode 100644 index 00000000..052da20e --- /dev/null +++ b/src/samarium/classes/fileio.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from enum import Enum +from os import write +from typing import IO, Any + +from samarium.utils import get_type_name + +from ..exceptions import SamariumIOError, SamariumTypeError +from .base import NULL, Array, Attrs, Int, Integer, Null, Slice, String + + +class Mode(Enum): + READ = "r" + WRITE = "w" + READ_WRITE = "r+" + APPEND = "a" + + +class FileManager: + @staticmethod + def create(path: String) -> Null: + open(path.val, "x").close() + return NULL + + @staticmethod + def open(path: String | Integer, mode: Mode, *, binary: bool = False) -> File: + if isinstance(path, Integer) and mode is Mode.READ_WRITE: + raise SamariumIOError( + "cannot open a standard stream in a read & write mode" + ) + f = open(path.val, mode.value + "b" * binary) + return File(f, mode.name, path.val, binary=binary) + + @staticmethod + def open_binary(path: String, mode: Mode) -> File: + return FileManager.open(path, mode, binary=True) + + @staticmethod + def quick( + path: String | File | Integer, + mode: Mode, + *, + data: String | Array | None = None, + binary: bool = False, + ) -> String | Array | Null: + if isinstance(path, String): + with open(path.val, mode.value + "b" * binary) as f: + if mode is Mode.READ: + if binary: + return Array(map(Int, f.read())) + return String(f.read()) + if data is None: + raise SamariumIOError("missing data") + if isinstance(data, Array): + bytes_ = b"" + for i in data.val: + try: + bytes_ += i.val.to_bytes(1, "big") + except AssertionError: + raise SamariumTypeError( + "some items in the array are not of type Integer" + ) from None + f.write(bytes_) + else: + f.write(data.val) + elif isinstance(path, Integer): + if mode in {Mode.APPEND, Mode.WRITE}: + fd = path.val + write(fd, str(data).encode()) + else: + raise SamariumIOError( + "reading from file descriptors is " + "not supported for quick operations" + ) + else: + file = path + if mode is Mode.READ: + return file.load() + if data is not None: + file.save(data) + else: + raise SamariumIOError("missing data") + return NULL + + +class File(Attrs): + __slots__ = ("binary", "mode", "path", "val") + + def __init__(self, file: IO, mode: str, path: str | int, *, binary: bool) -> None: + self.binary = binary + self.mode = mode + self.path = path + self.val = file + + def __bool__(self) -> bool: + return not self.val.closed + + def __str__(self) -> str: + return f"File(path:{self.path}, mode:{self.mode})" + + def __invert__(self) -> Null: + self.val.close() + return NULL + + def __getitem__(self, index: Any) -> Array | String | Integer | Null: + if isinstance(index, Slice): + if index.is_empty(): + return Int(self.val.tell()) + if isinstance(index.step, Integer): + raise SamariumIOError("cannot use step") + if isinstance(index.start, Integer): + if not isinstance(index.stop, Integer): + return self.load(index.start) + current_pos = self.val.tell() + self.val.seek(index.start.val) + data = self.val.read(index.stop.val - index.start.val) + self.val.seek(current_pos) + if self.binary: + if isinstance(data, bytes): + data = [*data] + return Array(map(Int, data)) + return String(data) + return self[Slice(Int(0), slice.stop, slice.step)] + else: + self.val.seek(index.val) + return NULL + + def load(self, bytes_: Integer | None = None) -> String | Array: + if bytes_ is None: + bytes_ = Int(-1) + val = self.val.read(bytes_.val) + if self.binary: + return Array(map(Int, val)) + return String(val) + + def save(self, data: String | Array) -> Null: + if (self.binary and isinstance(data, String)) or ( + not self.binary and isinstance(data, Array) + ): + raise SamariumTypeError(get_type_name(data)) + if isinstance(data, Array): + bytes_ = b"" + for i in data.val: + try: + bytes_ += i.val.to_bytes(1, "big") + except AssertionError: + raise SamariumTypeError( + "some items in the array are not of type Integer" + ) from None + self.val.write(bytes_) + else: + self.val.write(data.val) + return NULL diff --git a/src/samarium/core.py b/src/samarium/core.py index 02573fe1..2fb5daec 100644 --- a/src/samarium/core.py +++ b/src/samarium/core.py @@ -1,123 +1,81 @@ -import os +from __future__ import annotations + +import importlib.machinery +import importlib.util import sys +from pathlib import Path -from datetime import datetime from dahlia import dahlia -from time import sleep as _sleep, time_ns -from types import GeneratorType from . import exceptions as exc -from .objects import ( - null, - Int, - class_attributes, +from .builtins import ( + correct_type, + dtnow, function, mkslice, + print_safe, + readline, + sleep, t, - verify_type, - Class, - String, - Integer, + throw, + timestamp, +) +from .classes import ( MISSING, + NEXT, + NULL, + Array, + Attrs, + Enum, + FileManager, + Int, + Integer, + Mode, Module, Null, + String, Table, - Array, - Enum_, - Mode, - FileManager, + UserAttrs, ) +from .imports import merge_objects, parse_string, resolve_path from .runtime import Runtime from .tokenizer import tokenize -from .transpiler import Transpiler, Registry -from .utils import readfile, silence_stdout, sysexit - -MODULE_NAMES = [ - "collections", - "datetime", - "iter", - "math", - "operator", - "random", - "string", - "types", -] - - -def dtnow() -> Array: - utcnow = datetime.utcnow() - now = datetime.now().timetuple() - utcnow_tt = utcnow.timetuple() - tz = now[3] - utcnow_tt[3], now[4] - utcnow_tt[4] - utcnow_tpl = utcnow_tt[:-3] + (utcnow.microsecond // 1000,) + tz - return Array(map(Int, utcnow_tpl)) - - -def import_module(data: str, reg: Registry): - - module_import = False - try: - name, object_string = data.split(".") - objects = object_string.split(",") - except ValueError: - name = data - objects = [] - module_import = True - name = name.removeprefix("sm_") - if name == "samarium": - sys.stderr.write(dahlia("&4[RecursionError]\n")) - return - try: - path = sys.argv[1][: sys.argv[1].rfind("/") + 1] or "." - except IndexError: # REPL - path = os.getcwd() + "/" - - if f"{name}.sm" not in os.listdir(path): - if name not in MODULE_NAMES: - raise exc.SamariumImportError(f"invalid module: {name}") - path = os.path.join(os.path.dirname(__file__), "modules") - - with silence_stdout(): - imported = run(readfile(f"{path}/{name}.sm"), Registry(globals())) - - if module_import: - reg.vars.update({f"sm_{name}": Module(name, imported.vars)}) - elif objects == ["*"]: - imported.vars = { - k: v - for k, v in imported.vars.items() - if k.startswith("sm_") - } - reg.vars.update(imported.vars) - else: - for obj in objects: - try: - reg.vars[obj] = imported.vars[obj] - except KeyError: - raise exc.SamariumImportError( - f"{obj.removeprefix('sm_')} is not a member of the {name} module" - ) - - -def print_safe(*args): - args = [*map(verify_type, args)] - return_args = args[:] - args = [i.sm_to_string() for i in args] - types = [*map(type, args)] - if tuple in types: - raise exc.SamariumSyntaxError("missing brackets") - if GeneratorType in types: - raise exc.SamariumSyntaxError("invalid comprehension") - print(*args) - if len(return_args) > 1: - return Array(return_args) - elif not return_args or types[0] is Null: - return null - return return_args[0] - - -def readline(prompt: str = "") -> String: - return String(input(prompt)) +from .transpiler import Registry, Transpiler +from .utils import silence_stdout, sysexit + + +def import_to_scope(data: str, reg: Registry) -> None: + modules = parse_string(data) + for mod in modules: + if mod.name == "samarium": + raise exc.SamariumRecursionError + path = resolve_path(mod.name) + with silence_stdout(): + if (path / f"{mod.name}.sm").exists(): + imported = run((path / f"{mod.name}.sm").read_text(), Registry({})) + else: + spec: importlib.machinery.ModuleSpec = ( + importlib.util.spec_from_file_location( + mod.name, str(path / f"{mod.name}.py") + ) + ) # type: ignore + module = importlib.util.module_from_spec(spec) + sys.modules[mod.name] = module + spec.loader.exec_module(module) # type: ignore + registry = { + f"sm_{k}": v + for k, v in vars(module).items() + if f"__export_{v}" in dir(v) + } + imported = Registry(registry) + + reg.vars.update(merge_objects(reg, imported, mod)) + + +def import_inline(data: str) -> Attrs: + reg = Registry({}) + import_to_scope(data, reg) + return reg.vars.popitem()[1] def run( @@ -128,41 +86,21 @@ def run( load_template: bool = True, quit_on_error: bool = True, ) -> Registry: - runtime_state = Runtime.quit_on_error Runtime.quit_on_error = quit_on_error code = Transpiler(tokenize(code), reg).transpile().output if load_template: - code = readfile(f"{os.path.dirname(__file__)}/template.py").replace( - "{{ CODE }}", code + code = ( + (Path(__file__).resolve().parent / "template.py") + .read_text() + .replace("{{ CODE }}", code) ) try: if debug: print(code) - Runtime.import_level += 1 reg.vars = globals() | reg.vars exec(code, reg.vars) - Runtime.import_level -= 1 except Exception as e: exc.handle_exception(e) Runtime.quit_on_error = runtime_state return reg - - -def sleep(*args: Integer): - if not args: - raise exc.SamariumTypeError("no argument provided for ,.,") - if len(args) > 1: - raise exc.SamariumTypeError(",., only takes one argument") - (time,) = args - if not isinstance(time.value, int): - raise exc.SamariumTypeError(",., only accepts integers") - _sleep(time.value / 1000) - - -def throw(message: str = ""): - raise exc.SamariumError(message) - - -def timestamp() -> Integer: - return Int(time_ns() // 1_000_000) diff --git a/src/samarium/exceptions.py b/src/samarium/exceptions.py index 1301469a..cda864e4 100644 --- a/src/samarium/exceptions.py +++ b/src/samarium/exceptions.py @@ -1,46 +1,87 @@ import sys -from dahlia import dahlia from re import compile -from .runtime import Runtime +from dahlia import Dahlia +from .runtime import Runtime +DAHLIA = Dahlia() SINGLE_QUOTED_NAME = compile(r"'(\w+)'") - - -def handle_exception(exception: Exception): +BAD_OP = compile( + r"'(.+)' not supported between instances of '(\w+)' and '(\w+)'" + r"|unsupported operand type\(s\) for (.+): '(\w+)' and '(\w+)'" +) +BAD_UOP = compile(r"bad operand type for unary (.+): '(\w+)'") +ARG_NOT_ITER = compile(r"argument of type '(\w+)' is not iterable") +NOT_CALLITER = compile(r"'(\w+)' object is not (\w+)") +OP_MAP = { + ">=": ">:", + "<=": "<:", + "//": "--", + "%": "---", + "*": "++", + "@": "><", + "** or pow()": "+++", +} + + +def clear_name(name: str) -> str: + return name.removeprefix("__").removeprefix("sm_") + + +def handle_exception(exception: Exception) -> None: exc_type = type(exception) - name = exc_type.__name__ + errmsg = str(exception) + name = "" if exc_type is NotDefinedError: - exception = NotDefinedError( - ".".join(i.removeprefix("sm_") for i in str(exception).split(".")) - ) + exception = NotDefinedError(".".join(map(clear_name, errmsg.split(".")))) if ( exc_type is TypeError - and "missing 1 required positional argument: 'self'" in str(exception) + and "missing 1 required positional argument: 'self'" in errmsg ): exception = SamariumTypeError("missing instance") - if exc_type is SyntaxError: + elif m := BAD_OP.match(errmsg): + op, lhs, rhs = filter(None, m.groups()) + op = OP_MAP.get(op, op) + exc_type = NotDefinedError + exception = exc_type(f"{clear_name(lhs)} {op} {clear_name(rhs)}") + elif m := BAD_UOP.match(errmsg): + op, type_ = m.groups() + exc_type = NotDefinedError + exception = exc_type(f"{op}{clear_name(type_)}") + elif m := NOT_CALLITER.match(errmsg): + exc_type = NotDefinedError + type_ = clear_name(m.group(1)) + template = "... _ ->? {}" if m.group(2) == "iterable" else "{}()" + exception = exc_type(template.format(type_)) + elif m := ARG_NOT_ITER.match(errmsg): + exc_type = NotDefinedError + type_ = clear_name(m.group(1)) + exception = exc_type(f"->? {type_}") + elif exc_type is SyntaxError: exception = SamariumSyntaxError( - f"invalid syntax at {int(str(exception).split()[-1][:-1])}" + f"invalid syntax at {int(errmsg.split()[-1][:-1])}" ) elif exc_type in {AttributeError, NameError}: - names = SINGLE_QUOTED_NAME.findall(str(exception)) + names = SINGLE_QUOTED_NAME.findall(errmsg) if names == ["entry"]: names = ["no entry point defined"] - exception = NotDefinedError( - ".".join(i.removeprefix("__").removeprefix("sm_") for i in names) + exc_type = NotDefinedError + exception = exc_type( + f"{clear_name(names[0])}<<>>{':' * (names[1] == '__setitem__')}" + if len(names) >= 2 and names[1] in {"__getitem__", "__setitem__"} + else ".".join(map(clear_name, names)) ) - name = "NotDefinedError" elif exc_type not in {AssertionError, NotDefinedError}: name = exc_type.__name__ if name.startswith("Samarium"): name = name.removeprefix("Samarium") else: - name = f"External{name}".replace("ExternalZeroDivision", "Math") - sys.stderr.write(dahlia(f"&4[{name}] {exception}\n")) + name = f"Python{name}".replace("PythonZeroDivision", "Math") + name = name or exc_type.__name__ + sys.stderr.write(DAHLIA.convert(f"&4[{name}] {exception}\n")) if Runtime.quit_on_error: - exit(1) + raise SystemExit(1) class SamariumError(Exception): @@ -48,7 +89,7 @@ class SamariumError(Exception): class NotDefinedError(SamariumError): - def __init__(self, inp: object, message: str = ""): + def __init__(self, inp: object, message: str = "") -> None: if isinstance(inp, str): message = inp inp = "" @@ -62,7 +103,9 @@ class SamariumImportError(SamariumError): class SamariumSyntaxError(SamariumError): - pass + def __init__(self, msg: str) -> None: + sys.stderr.write(DAHLIA.convert(f"&4[SyntaxError] {msg}\n")) + raise SystemExit(1) class SamariumTypeError(SamariumError): @@ -75,3 +118,7 @@ class SamariumValueError(SamariumError): class SamariumIOError(SamariumError): pass + + +class SamariumRecursionError(SamariumError): + pass diff --git a/src/samarium/handlers.py b/src/samarium/handlers.py deleted file mode 100644 index d3c9cf00..00000000 --- a/src/samarium/handlers.py +++ /dev/null @@ -1,162 +0,0 @@ -from .tokens import Token - - -class Scroller: - def __init__(self, program: str): - self.program = program - self.prev = "" - - @property - def pointer(self) -> str: - return self.program[:1] - - def next(self, offset: int = 1) -> str: - return self.program[offset:offset + 1] - - def shift(self, units: int = 1): - self.prev = self.program[:units][-1] - self.program = self.program[units:] - - -def plus(scroller: Scroller) -> Token: - if scroller.next() == "+": - return Token.POW if scroller.next(2) == "+" else Token.MUL - return Token.ADD - - -def minus(scroller: Scroller) -> Token: - tokens = { - ">": Token.IN if scroller.next(2) == "?" else Token.TO, - "-": Token.MOD if scroller.next(2) == "-" else Token.DIV, - } - return tokens.get(scroller.next(), Token.SUB) - - -def colon(scroller: Scroller) -> Token: - if scroller.next() == ":": - return Token.NE if scroller.next(2) == ":" else Token.EQ - return Token.ASSIGN - - -def less(scroller: Scroller) -> Token: - if scroller.next() == "~": - if scroller.next(2) == "~": - return Token.FILE_READ - elif scroller.next(2) == ">": - return Token.FILE_READ_WRITE - elif scroller.next(2) == "%": - return Token.FILE_BINARY_READ - elif scroller.next() == "%": - if scroller.next(2) == ">": - return Token.FILE_BINARY_READ_WRITE - tokens = { - "-": Token.FROM, - ">": Token.DEFAULT, - "<": Token.SLICE_OPEN, - ":": Token.LE, - "%": Token.FILE_QUICK_BINARY_READ, - "~": Token.FILE_QUICK_READ, - } - return tokens.get(scroller.next(), Token.LT) - - -def greater(scroller: Scroller) -> Token: - if scroller.program[:3] == ">==": - return Token.COMMENT_CLOSE - tokens = {">": Token.SLICE_CLOSE, ":": Token.GE} - return tokens.get(scroller.next(), Token.GT) - - -def equal(scroller: Scroller) -> Token: - if scroller.next() == "=": - return Token.COMMENT_OPEN if scroller.next(2) == "<" else Token.COMMENT - if scroller.next() == ">": - return Token.EXIT if scroller.next(2) == "!" else Token.ENTRY - raise ValueError("invalid token") - - -def dot(scroller: Scroller) -> Token: - if scroller.next() == ".": - return Token.FOR if scroller.next(2) == "." else Token.WHILE - return Token.ATTR - - -def question(scroller: Scroller) -> Token: - if scroller.next() == "?": - return Token.READLINE if scroller.next(2) == "?" else Token.TRY - elif scroller.next() == "~": - if scroller.next(2) == ">": - return Token.FILE_CREATE - return Token.TYPE if scroller.next() == "!" else Token.IF - - -def exclamation(scroller: Scroller) -> Token: - if scroller.next() == "!": - return Token.THROW if scroller.next(2) == "!" else Token.CATCH - return Token.PARENT if scroller.next() == "?" else Token.PRINT - - -def pipe(scroller: Scroller) -> Token: - return Token.OR if scroller.next() == "|" else Token.BOR - - -def ampersand(scroller: Scroller) -> Token: - if scroller.program[:4] == "&~~>": - return Token.FILE_APPEND - elif scroller.program[:4] == "&%~>": - return Token.FILE_BINARY_APPEND - elif scroller.program[:3] == "&~>": - return Token.FILE_QUICK_APPEND - elif scroller.program[:3] == "&%>": - return Token.FILE_QUICK_BINARY_APPEND - return Token.AND if scroller.next() == "&" else Token.BAND - - -def tilde(scroller: Scroller) -> Token: - if scroller.next() == "~" and scroller.next(2) == ">": - return Token.FILE_WRITE - tokens = {">": Token.FILE_QUICK_WRITE, "~": Token.NOT} - return tokens.get(scroller.next(), Token.BNOT) - - -def caret(scroller: Scroller) -> Token: - return Token.XOR if scroller.next() == "^" else Token.BXOR - - -def comma(scroller: Scroller) -> Token: - if scroller.program[1:3] == ".,": - return Token.SLEEP - return Token.ELSE if scroller.next() == "," else Token.SEP - - -def open_brace(scroller: Scroller) -> Token: - return Token.TABLE_OPEN if scroller.next() == "{" else Token.BRACE_OPEN - - -def close_brace(scroller: Scroller) -> Token: - return Token.TABLE_CLOSE if scroller.next() == "}" else Token.BRACE_CLOSE - - -def hash_(scroller: Scroller) -> Token: - return Token.HASH if scroller.next() == "#" else Token.ENUM - - -def asterisk(scroller: Scroller) -> Token: - return Token.YIELD if scroller.next() == "*" else Token.FUNCTION - - -def percent(scroller: Scroller) -> Token: - if scroller.next() == ">": - return Token.FILE_QUICK_BINARY_WRITE - elif scroller.next() == "~": - if scroller.next(2) == ">": - return Token.FILE_BINARY_WRITE - return Token.CAST - - -def at(scroller: Scroller) -> Token: - if scroller.next() == "@": - if scroller.next(2) == "@": - return Token.ARR_STMP - return Token.UNIX_STMP - return Token.CLASS diff --git a/src/samarium/imports.py b/src/samarium/imports.py new file mode 100644 index 00000000..80b0eb84 --- /dev/null +++ b/src/samarium/imports.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from re import Pattern, compile, sub +from sys import argv + +from .classes import Attrs, Module +from .exceptions import SamariumImportError, SamariumSyntaxError +from .transpiler import Registry + +FORMATTERS = { + r"\bsm_(\w+)\b": r"\g<1>", + r"\.?Array\(\[": ".[", + r"\]\)": "]", +} + +MODULE_NAMES = [ + "collections", + "datetime", + "io", + "iter", + "math", + "operator", + "random", + "string", + "types", +] + + +def parse_string(string: str) -> list[Mod]: + string = format_string(string) + for it in Import.__members__.values(): + imptype, pattern = it, it.value + if pattern.match(string): + break + else: + raise SamariumSyntaxError("invalid import syntax") + if imptype is Import.MODULE: + mods = string.split(",") + out = [] + for m in mods: + name, alias = m.split(":") if ":" in m else (m, None) + out.append(Mod(name, alias)) + return out + elif imptype is Import.STAR: + return [Mod(string.split(".")[0], None, True)] + elif imptype is Import.OBJECT: + mod, obj = string.split(".") + obj, alias = obj.split(":") if ":" in obj else (obj, None) + return [Mod(mod, None, [Obj(obj, alias)])] + else: + mod, objects_ = string.split(".") + objects = objects_.strip("[]").split(",") + objs = [] + for o in objects: + name, alias = o.split(":") if ":" in o else (o, None) + objs.append(Obj(name, alias)) + return [Mod(mod, None, objs)] + + +def format_string(string: str) -> str: + for pattern, repl in FORMATTERS.items(): + string = sub(pattern, repl, string) + return string + + +def merge_objects(reg: Registry, imported: Registry, module: Mod) -> dict[str, Attrs]: + vars_ = reg.vars.copy() + if module.objects is False: + vars_[f"sm_{module.alias}"] = Module(module.name, imported.vars) + elif module.objects is True: + vars_ |= {k: v for k, v in imported.vars.items() if k.startswith("sm_")} + else: + for obj in module.objects: + try: + vars_[f"sm_{obj.alias}"] = imported.vars[f"sm_{obj.name}"] + except KeyError: + raise SamariumImportError( + f"{obj.name} is not a member of the {module.name} module" + ) from None + return vars_ + + +def regex(string: str) -> Pattern: + return compile("^" + string.replace("W", r"\w+") + "$") + + +def resolve_path(name: str) -> Path: + try: + path = Path(argv[1][: argv[1].rfind("/") + 1] or ".") + except IndexError: # REPL + path = Path().resolve() + paths = [e.name for e in path.iterdir()] + if not (f"{name}.sm" in paths or f"{name}.py" in paths): + if name not in MODULE_NAMES: + raise SamariumImportError(f"invalid module: {name}") + path = Path(__file__).resolve().parent / "modules" + return path + + +class Import(Enum): + MODULE = regex(r"W(?::W)?(?:,W(?::W)?)*") + STAR = regex(r"W\.\*") + OBJECT = regex(r"W\.W(?::W)?") + OBJECTS = regex(r"W\.\[W(?::W)?(?:,W(?::W)?)*\]") + + +class ImportAttrs: + name: str + alias: str | None + + def __post_init__(self) -> None: + if self.alias is None: + self.alias = self.name + + +@dataclass +class Mod(ImportAttrs): + name: str + alias: str | None = None + objects: list[Obj] | bool = False + + +@dataclass +class Obj(ImportAttrs): + name: str + alias: str | None = None diff --git a/src/samarium/modules/collections.sm b/src/samarium/modules/collections.sm index dfdc1f79..173635a1 100644 --- a/src/samarium/modules/collections.sm +++ b/src/samarium/modules/collections.sm @@ -1,9 +1,8 @@ -<-operator; +<=operator; @ Stack { - - create size? * { + => size? * { size <> -/; 'stack: []; 'size: size; @@ -11,7 +10,7 @@ push item * { ? 'is_full() { - "stack is full (size " + 'size.to_string() + ")"!!!; + "stack is full (size " + ""?!('size) + ")"!!!; } 'stack+: [item]; } @@ -22,9 +21,7 @@ } } - special * { - * 'stack$; - } + $ * { * 'stack$; } pop * { ? 'is_empty() { @@ -42,56 +39,36 @@ * 'stack<<-/>>; } - to_bit * { - * 'stack$ > \; - } - - is_full * { - * 'size :: 'stack$; - } - - is_empty * { - * 'stack$ :: \; - } + ? * { * 'stack$ > \; } + is_full * { * 'size :: 'stack$; } + is_empty * { * 'stack$ :: \; } - to_string * { - <-string.wrap; - <-types.String; + ! * { ?? { top: 'peek(); } !! { top:; } - ? top?! :: String { - top: wrap(top.to_string(), "\""); + ? top?! :: <-types.String { + top: <-string.wrap(""?!(top), "\""); } - * "Stack(capacity:" + 'size.to_string() - + ", size:" + 'stack$.to_string() - + ", top:" + top.to_string() + * "Stack(capacity:" + ""?!('size) + + ", size:" + ""?!('stack$) + + ", top:" + ""?!(top) + ")"; } - } @ Queue { - - create size? * { + => size? * { size <> -/; 'queue: []; 'size: size; } - special * { - * 'queue$; - } - - is_empty * { - * ~~ '$; - } - - is_full * { - * '$ :: 'size; - } + $ * { * 'queue$; } + is_empty * { * ~~ '$; } + is_full * { * '$ :: 'size; } throw_empty * { ? 'is_empty() { @@ -101,7 +78,7 @@ put item * { ? 'is_full() { - "queue is full (size " + 'size.to_string() + ")"!!!; + "queue is full (size " + ""?!('size) + ")"!!!; } 'queue+: [item]; } @@ -119,61 +96,44 @@ * out; } - first * { - 'throw_empty(); * 'queue<<\>>; - } - last * { - 'throw_empty(); * 'queue<<-/>>; - } - - to_bit * { - * '$ > \; - } + first * { 'throw_empty(); * 'queue<<\>>; } + last * { 'throw_empty(); * 'queue<<-/>>; } + ? * { * '$ > \; } - to_string * { - <-string.wrap; - <-types.String; + ! * { + <=string.wrap; + <=types.String; ? '$ <: /\/ { - items: 'queue.to_string(); - } ,, ? '$ > /\/ { - first: 'first().to_string(); - last: 'last().to_string(); + items: ""?!('queue)<>; + } ,, { + first: ""?!('first()); + last: ""?!('last()); ? 'first()?! :: String { first: wrap(first, "\""); } ? 'last()?! :: String { last: wrap(last, "\""); } - items: "[" + first.to_string() + ", ..., " + last.to_string() + "]"; - } ,, { - items: "[]"; + items: ""?!(first) + ", ..., " + ""?!(last); } - * "Queue(capacity:" + 'size.to_string() - + ", size:" + 'queue$.to_string() - + ", items:" + items + ")"; + * "Queue(" + items + ")"; } - } @ Set { - - create items? capacity? * { + => items? capacity? * { items <> []; capacity <> -/; - <-iter.find; - 'find: find; 'items: [i ... i ->? items] ? items ,, []; 'capacity: capacity; } - special * { - * 'items$; - } + $ * { * 'items$; } add value * { - ? ~~ 'has(value) { + ? value ~~ ->? ' { ? 'is_full() { - "set is full (capacity " + 'capacity.to_string() + ")"!!!; + "set is full (capacity " + ""?!('capacity) + ")"!!!; } 'items+: [value]; * /; @@ -181,68 +141,64 @@ * \; } - to_bit * { - * ~~ 'is_empty(); - } - - to_string * { - * "Set(capacity:" + 'capacity.to_string() - + ", size:" + '$.to_string() - + ", items:" + 'items.to_string() - + ")"; - } - - remove value * { - 'items-: [value]; - } - - has value * { - * 'find('items, value) >: \; - } + ? * { * ~~ 'is_empty(); } - values * { - * 'items; + ! * { + * "Set(" + ""?!('items)<> + ")"; } - clear * { - 'items: []; - } + remove value * { 'items-: [value]; } - is_empty * { - * '$ :: \; + ->? value * { + * <-iter.find('items, value) >: \; } - is_full * { - * '$ :: 'capacity; + #check_type value * { + ? value?! ::: Set()?! { + "expected Set, received " + ""?!(value?!)!!!; + } } - new_set_size a b * { + clear * { 'items: []; } + is_empty * { * '$ :: \; } + is_full * { * '$ :: 'capacity; } + #new_set_size a b * { + ? <-math.min([a.capacity, b.capacity]) :: -/ { * -/; } * a$ ? a$ > b$ ,, b$; } + ::: other * { * ~~ ' :: other; } + :: other * { + sort: <-iter.sorted; + * sort('items) :: sort(other.items); + } - union other * { - ? other?! ::: Set()?! { - "expected Set, received " + other?!.to_string()!!!; - } + | other * { + '#check_type(other); - union_set: Set('values(), 'new_set_size(', other) ++ /\); + nss: '#new_set_size(', other); + ? nss > \ { nss++: /\; } + union_set: Set('items, nss); - ... value ->? other.values() { + ... value ->? other.items { union_set.add(value); } * union_set; } - intersection other * { - ? other?! ::: Set()?! { - "expected Set, received " + other?!.to_string()!!!; - } + ^ other * { + '#check_type(other); - intersection_set: Set([], 'new_set_size(', other)); + * (' - other) | (other - '); + } - ... value ->? 'values() { - ? other.has(value) { + & other * { + '#check_type(other); + + intersection_set: Set([], '#new_set_size(', other)); + + ... value ->? 'items { + ? value ->? other { intersection_set.add(value); } } @@ -250,15 +206,13 @@ * intersection_set; } - difference other * { - ? other?! ::: Set()?! { - "expected Set, received " + other?!.to_string()!!!; - } + - other * { + '#check_type(other); - difference_set: Set([], 'new_set_size(', other)); + difference_set: Set([], '#new_set_size(', other)); - ... value ->? 'values() { - ? ~~ other.has(value) { + ... value ->? 'items { + ? value ~~ ->? other { difference_set.add(value); } } @@ -266,40 +220,31 @@ * difference_set; } - is_subset other * { - ? other?! ::: Set()?! { - "expected Set, received " + other?!.to_string()!!!; - } + > other * { + '#check_type(other); - ? '$ > other$ { * \; } + ? '$ <: other$ { * \; } - ... value ->? 'values() { - ? ~~ other.has(value) { * \; } + ... value ->? other.items { + ? value ~~ ->? ' { * \; } } * /; } + >: other * { * ' :: other || ' > other; } } @ Deque { - create size? * { + => size? * { size <> -/; 'deque: []; 'size: size; } - special * { - * 'deque$; - } - - is_empty * { - * ~~ '$; - } - - is_full * { - * '$ :: 'size; - } + $ * { * 'deque$; } + is_empty * { * ~~ '$; } + is_full * { * '$ :: 'size; } throw_empty * { ? 'is_empty() { @@ -309,7 +254,7 @@ throw_full * { ? 'is_full() { - "deque is full (size " + 'size.to_string() + ")"!!!; + "deque is full (size " + ""?!('size) + ")"!!!; } } @@ -317,9 +262,7 @@ 'throw_full(); 'deque: [item] + 'deque; } - put item * { - 'throw_full(); 'deque+: [item]; - } + put item * { 'throw_full(); 'deque+: [item]; } put_front_all items * { ... item ->? items { @@ -333,17 +276,9 @@ } } - front * { - * 'deque<<\>>; - } - - back * { - * 'deque<<-/>>; - } - - to_bit * { - * '$ > \; - } + front * { * 'deque<<\>>; } + back * { * 'deque<<-/>>; } + ? * { * '$ > \; } get_front * { 'throw_empty(); @@ -359,94 +294,82 @@ * out; } - to_string * { - <-string.wrap; - <-types.String; + ! * { + <=string.wrap; + <=types.String; ? '$ <: /\/ { - items: 'deque.to_string(); - } ,, ? '$ > /\/ { - front: 'front().to_string(); - back: 'back().to_string(); + items: ""?!('deque)<>; + } ,, { + front: ""?!('front()); + back: ""?!('back()); ? 'front()?! :: String { front: wrap(front, "\""); } ? 'back()?! :: String { back: wrap(back, "\""); } - items: "[" + front.to_string() + ", ..., " + back.to_string() + "]"; - } ,, { - items: "[]"; + items: ""?!(front) + ", ..., " + ""?!(back); } - * "Deque(capacity:" + 'size.to_string() - + ", size:" + 'deque$.to_string() - + ", items:" + items + ")"; + * "Deque(" + items + ")"; } - } @ StaticArray { - - create value type? * { + => value type? * { type <>; - <-types.Array, Integer; - ? value?! :: Integer { + ? value?! :: <-types.Integer { 'value: {{}}; 'size: value; 'type: type; - } ,, ? value?! :: Array { + } ,, ? value?! :: <-types.Array { 'value: {{}}; 'size: value$; 'type: type; i: \; ... e ->? value { ? e?! ::: type ::: { - "expected " + type.to_string() + ", found " + e?!.to_string()!!!; + "expected " + ""?!(type) + ", found " + ""?!(e?!)!!!; } 'value<>: e; i+: /; } } ,, { - "expected Integer or Array, found " + value?!.to_string()!!!; + "expected Integer or Array, found " + ""?!(value?!)!!!; } } - special * { - * 'value$; - } + $ * { * 'value$; } - to_string * { - * "StaticArray(size:" + 'size.to_string() - + ", type:" + 'type.to_string() - + ", items:" + 'value.to_string() + ! * { + * "StaticArray(size:" + ""?!('size) + + ", type:" + ""?!('type) + + ", items:" + ""?!('value) + ")"; } - to_bit * { - * 'value.to_bit(); - } + ? * { * / ? 'value ,, \; } throw_invalid_index arg * { - <-types.Integer; - ? arg?! ::: Integer { - "expected Integer, received " + arg?!.to_string()!!!; + ? arg?! ::: <-types.Integer { + "expected Integer, received " + ""?!(arg?!)!!!; } ? ~~ (\ <: arg < 'size) { - "index " + arg.to_string() + " is out of bounds (size " + 'size.to_string() + ")"!!!; + "index " + ""?!(arg) + " is out of bounds (size " + ""?!('size) + ")"!!!; } } throw_invalid_type type * { ? type ::: 'type ::: { - "expected " + 'type.to_string() + ", received " + type.to_string()!!!; + "expected " + ""?!('type) + ", received " + ""?!(type)!!!; } } - get_item index * { + <<>> index * { 'throw_invalid_index(index); * 'value<>; } - set_item index value * { + <<>>: index value * { 'throw_invalid_index(index); 'throw_invalid_type(value?!); 'value<>: value; @@ -455,8 +378,7 @@ } @ ArithmeticArray { - - create array * { + => array * { 'array: array; } @@ -464,125 +386,33 @@ * ArithmeticArray([op(i, other) ... i ->? 'array]); } - and other * { - * 'apply(operator.and, other); - } - - add other * { - * 'apply(operator.add, other); - } - - divide other * { - * 'apply(operator.divide, other); - } - - equals other * { - * 'apply(operator.equals, other); - } - - greater_than_or_equal other * { - * 'apply(operator.greater_than_or_equal, other); - } - - greater_than other * { - * 'apply(operator.greater_than, other); - } - - less_than_or_equal other * { - * 'apply(operator.less_than_or_equal, other); - } - - less_than other * { - * 'apply(operator.less_than, other); - } - - mod other * { - * 'apply(operator.mod, other); - } - - multiply other * { - * 'apply(operator.multiply, other); - } - - not_equals other * { - * 'apply(operator.not_equals, other); - } - - or other * { - * 'apply(operator.or, other); - } - - power other * { - * 'apply(operator.power, other); - } - - subtract other * { - * 'apply(operator.subtract, other); - } - - xor other * { - * 'apply(operator.xor, other); - } - - and_assign other * { - 'array: 'and(other).array; * '; - } - - add_assign other * { - 'array: 'add(other).array; * '; - } - - divide_assign other * { - 'array: 'divide(other).array; * '; - } - - mod_assign other * { - 'array: 'mod(other).array; * '; - } - - multiply_assign other * { - 'array: 'multiply(other).array; * '; - } - - or_assign other * { - 'array: 'or(other).array; * '; - } - - power_assign other * { - 'array: 'power(other).array; * '; - } - - subtract_assign other * { - 'array: 'subtract(other).array; * '; - } - - xor_assign other * { - 'array: 'xor(other).array; * '; - } - - get_item item * { - <-types.Array; + & other * { * 'apply(operator.and, other); } + + other * { * 'apply(operator.add, other); } + -- other * { * 'apply(operator.div, other); } + :: other * { * 'apply(operator.eq, other); } + >: other * { * 'apply(operator.ge, other); } + > other * { * 'apply(operator.gt, other); } + <: other * { * 'apply(operator.le, other); } + < other * { * 'apply(operator.lt, other); } + --- other * { * 'apply(operator.mod, other); } + ++ other * { * 'apply(operator.mul, other); } + ::: other * { * 'apply(operator.ne, other); } + | other * { * 'apply(operator.or, other); } + +++ other * { * 'apply(operator.pow, other); } + - other * { * 'apply(operator.sub, other); } + ^ other * { * 'apply(operator.xor, other); } + + <<>> item * { out: 'array<>; - ? out?! :: Array { + ? out?! :: <-types.Array { * ArithmeticArray(out); } * out; } - set_item item value * { - 'array<>: value; - } - - special * { - * 'array$; - } - - to_bit * { - * 'array.to_bit(); - } - - to_string * { - * 'array.to_string(); - } + <<>>: item value * { 'array<>: value; } + $ * { * 'array$; } + ? * { * / ? 'array ,, \; } + ! * { * "A" + ""?!('array); } } \ No newline at end of file diff --git a/src/samarium/modules/datetime.sm b/src/samarium/modules/datetime.sm index 36f27814..a9ec9acb 100644 --- a/src/samarium/modules/datetime.sm +++ b/src/samarium/modules/datetime.sm @@ -1,4 +1,4 @@ -<-types.String, Table; +<=types.String; MONTHS: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; WEEKDAYS: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; @@ -57,15 +57,17 @@ weekday_name n * { } timestamp_utc ts? * { + 60_000: ///\/\/\\//\\\\\; + 3_600_000: //\//\///\///\/\\\\\\\; ts <> @@; dtnow: @@@; tz: dtnow<<-/\..>>; - offset: tz<<\>> ++ //\//\///\///\/\\\\\\\ + tz<> ++ ///\/\/\\//\\\\\; + offset: tz<<\>> ++ 3_600_000 + tz<> ++ 60_000; * ts - offset; } @ DateTime { - create Y? M? D? h? m? s? ms? tz? * { + => Y? M? D? h? m? s? ms? tz? * { Y <>; ? :: Y { 'array: @@@; @@ -98,7 +100,7 @@ timestamp_utc ts? * { } } - subtract other * { + - other * { diff: 'to_timestamp() - other.to_timestamp(); 1000: /\/\ +++ //; 60: ////\\; @@ -111,31 +113,25 @@ timestamp_utc ts? * { * DTDiff([diff, h, m, s, ms]); } - to_string * { - <-string.format, leftpad; - add0s n c * { * leftpad(String(n), c, "0"); } - * format( - "$Y-$M-$D $h:$m:$s.$z", - {{ - "Y" -> 'year, - "M" -> add0s('month, /\), - "D" -> add0s('day, /\), - "h" -> add0s('hour + 'timezone<<\>>, /\), - "m" -> add0s('minute + 'timezone<>, /\), - "s" -> add0s('second, /\), - "z" -> add0s('millisecond, //) - }} - ); + ! * { + add0s n c * { * <-string.leftpad(String(n), c, "0"); } + * "$Y-$M-$D $h:$m:$s.$z" --- {{ + "Y" -> 'year, + "M" -> add0s('month, /\), + "D" -> add0s('day, /\), + "h" -> add0s('hour + 'timezone<<\>>, /\), + "m" -> add0s('minute + 'timezone<>, /\), + "s" -> add0s('second, /\), + "z" -> add0s('millisecond, //) + }}; } to_timestamp * { - <-collections.ArithmeticArray; - 1000: /\/\ +++ //; 60: ////\\; mod_array: 'array<<..-/\..>>; - array: ArithmeticArray(mod_array<<....-/>>); + array: <-collections.ArithmeticArray(mod_array<<....-/>>); total: \; month_days: days_in_month('year, 'month); @@ -152,30 +148,27 @@ timestamp_utc ts? * { * total; } - } @ DTDiff { - - create data * { + => data * { 'data: data; } - to_string * { - <-string.leftpad, strip_left; - add0s n c * { * leftpad(String(n), c, "0"); } + ! * { + add0s n c * { * <-string.leftpad(String(n), c, "0"); } D: 'data<<\>>; h: 'data<>; m: 'data<>; s: 'data<>; ms: 'data<>; out: ""; - ? D { out+: D.to_string() + " days"; } + ? D { out+: ""?!(D) + " days"; } ? D && (~~ (h || m || s || ms)) { out+: ", "; } ? h { out+: add0s(h, /\) + ":"; } ? h || m { out+: add0s(m, /\) + ":"; } ? h || m || s { out+: add0s(s, /\); } ? ms { out+: "." + add0s(ms, //); } - * strip_left(out, "0") || "0"; + * <-string.strip_left(out, "0") || "0"; } } diff --git a/src/samarium/modules/io.sm b/src/samarium/modules/io.sm new file mode 100644 index 00000000..8a09728e --- /dev/null +++ b/src/samarium/modules/io.sm @@ -0,0 +1,82 @@ +<=math.to_hex; +<=iter.[any, map]; +<=string.[join, split, strip]; +<=types.[Array, Integer, Slice]; + + +@ Bytes { + => value * { + '#value: '#to_bytes(value); + } + + #to_bytes v * { + ? v?! :: ""?! { + * [i% ... i ->? v]; + } + ? v?! :: []?! { + ? any([i?! ::: /?! ... i ->? v]) { + "non-int found in the list"!!!; + } + * v; + } ,, { + "invalid type: " + ""?!(v?!)!!!; + } + } + + + other * { + ? other?! :: Bytes { + * Bytes('#value + other.export()); + } + * Bytes('#value + '#to_bytes(other)); + } + + ! * { + * join([to_hex(i) ... i ->? '#value], " "); + } + + export * { * '#value; } + + export_string * { + * join([i% ... i ->? '#value]); + } +} + +read_until target? * { + target <> ""; + s: ""; + .. { + i: ???; + s+: i; + ? i :: target { <- } + s+: "\n"; + } + * s; +} + +#autocast value * { + value: strip(value); + ? ~~ value { *; } + ? ".." ->? value { + bits: [Integer(i) ? i ,, ... i ->? split(value, "..")]; + ? bits$ > // { + "too many arguments for slice"!!!; + } + * Slice(**bits); + } + ? "=" ->? value { + items: [split(i, "=") ... i ->? split(value, ",")]; + * {{strip(k) -> #autocast(v) ... k, v ->? items}}; + } + ... sep ->? ", " { + ? sep ->? value { + * Array(map(#autocast, split(value, sep))); + } + } + ?? { * Integer(value); } + !! { * value; } +} + +inputcast prompt? * { + prompt <> ""; + * #autocast(prompt???); +} diff --git a/src/samarium/modules/iter.sm b/src/samarium/modules/iter.sm index da0a43d2..774e2072 100644 --- a/src/samarium/modules/iter.sm +++ b/src/samarium/modules/iter.sm @@ -1,7 +1,6 @@ find array target * { - <-types.String; index: \; - ? array?! :: String { + ? array?! :: <-types.String { .. index + target$ <: array$ { ? array<> :: target { * index; @@ -30,7 +29,16 @@ count array target * { * s; } -filter array function * { +cycle iter * { + saved: [e ... e ->? iter]; + .. { + ... e ->? saved { + ** e; + } + } +} + +filter function array * { ... e ->? array { ? function(e) { ** e; @@ -38,14 +46,12 @@ filter array function * { } } -filter_false array function * { - flip arg * { - * ~~ function(arg); - } - * filter(array, flip); +filter_false function array * { + flip arg * { * ~~ function(arg); } + * filter(flip, array); } -map array function * { +map function array * { ... e ->? array { ** function(e); } @@ -100,31 +106,14 @@ reduce array function * { * prev; } -enumerate array * { - i: \; - ... e ->? array { - ** [i, e]; - i+: /; - } -} - pairwise array * { ... i ->? <<..array$ - />> { ** array<>; } } -zip arrays... * { - <-math.min; - shortest: min([i$ ... i ->? arrays]); - ... i ->? <<..shortest>> { - ** [it<> ... it ->? arrays]; - } -} - zip_longest fill arrays... * { - <-math.max; - longest: max([i$ ... i ->? arrays]); + longest: <-math.max([i$ ... i ->? arrays]); ... i ->? <<..longest>> { ... it ->? arrays { ?? { @@ -137,8 +126,7 @@ zip_longest fill arrays... * { } find_all array target * { - <-types.String; - ? array?! :: String && target$ > / { + ? array?! :: <-types.String && target$ > / { ... i ->? <<..array$>> { substring: array<>; ? substring :: target { @@ -146,7 +134,7 @@ find_all array target * { } } } ,, { - ... i, v ->? enumerate(array) { + ... i, v ->? <<>> >< array { ? v :: target { ** i; } diff --git a/src/samarium/modules/math.sm b/src/samarium/modules/math.sm index d1d5f5fd..c22de3e2 100644 --- a/src/samarium/modules/math.sm +++ b/src/samarium/modules/math.sm @@ -1,7 +1,5 @@ abs n * { - ? n < \ { - * -n; - } + ? n < \ { * -n; } * n; } @@ -18,42 +16,32 @@ factorial n * { } gcd a b * { - .. b { - a, b: b, a --- b; - } + .. b { a, b: b, a --- b; } * abs(a); } lcm a b * { - ?? { - * abs(a ++ b) -- gcd(a, b); - } !! { - * \; - } + ?? { * abs(a ++ b) -- gcd(a, b); } + !! { * \; } } product array * { o: /; - ... e ->? array { - o++: e; - } + ... e ->? array { o++: e; } * o; } -sum array * { - o: \; - ... e ->? array { - o+: e; - } +sum array start? * { + start <> \; + o: start; + ... e ->? array { o+: e; } * o; } max array * { o: array<<\>>; ... e ->? array { - ? e > o { - o: e; - } + ? e > o { o: e; } } * o; } @@ -61,9 +49,7 @@ max array * { min array * { o: array<<\>>; ... e ->? array { - ? e < o { - o: e; - } + ? e < o { o: e; } } * o; } @@ -96,38 +82,38 @@ shr a b * { } to_hex n * { - <-string.HEXDIGITS; + <=string.HEXDIGITS; div: n -- /\\\\; mod: n --- /\\\\; - ? ~~ div { - * HEXDIGITS<>; - } + ? ~~ div { * HEXDIGITS<>; } * to_hex(div) + HEXDIGITS<>; } to_oct n * { - <-string.OCTDIGITS; + <=string.OCTDIGITS; div: n -- /\\\; mod: n --- /\\\; - ? ~~ div { - * OCTDIGITS<>; - } + ? ~~ div { * OCTDIGITS<>; } * to_oct(div) + OCTDIGITS<>; } is_prime n * { - ? n <: // { + 0: \; + 2: /\; + 3: //; + 6: 2 ++ 3; + ? n <: 3 { * n > /; } - ? n --- /\ :: \ || n --- // :: \ { - * \; + ? n --- 2 :: 0 || n --- 3 :: 0 { + * 0; } i: /\/; - .. i +++ /\ <: n { - ? n --- i :: \ || n --- (i + /\) :: \ { - * \; + .. i +++ 2 <: n { + ? n --- i :: 0 || n --- (i + 2) :: 0 { + * 0; } - i+: //\; + i+: 6; } * /; } \ No newline at end of file diff --git a/src/samarium/modules/operator.sm b/src/samarium/modules/operator.sm index e1ef8bb8..2e5b29fc 100644 --- a/src/samarium/modules/operator.sm +++ b/src/samarium/modules/operator.sm @@ -10,19 +10,19 @@ cast x * { * x%; } -divide x y * { +div x y * { * x -- y; } -equals x y * { +eq x y * { * x :: y; } -greater_than_or_equal x y * { +ge x y * { * x >: y; } -greater_than x y * { +gt x y * { * x > y; } @@ -34,11 +34,11 @@ hash x * { * x##; } -less_than_or_equal x y * { +le x y * { * x <: y; } -less_than x y * { +lt x y * { * x < y; } @@ -46,7 +46,7 @@ mod x y * { * x --- y; } -multiply x y * { +mul x y * { * x ++ y; } @@ -54,7 +54,7 @@ not x * { * ~x; } -not_equals x y * { +ne x y * { * x ::: y; } @@ -62,7 +62,7 @@ or x y * { * x | y; } -power x y * { +pow x y * { * x +++ y; } @@ -74,16 +74,16 @@ special x * { * x$; } -subtract x y * { +sub x y * { * x - y; } to_bit x * { - * x.to_bit(); + * / ? x ,, \; } to_string x * { - * x.to_string(); + * ""?!(x); } xor x y * { diff --git a/src/samarium/modules/random.sm b/src/samarium/modules/random.sm index 12f9f331..2af18e64 100644 --- a/src/samarium/modules/random.sm +++ b/src/samarium/modules/random.sm @@ -14,11 +14,7 @@ shuffle array * { } choices array k * { - o: []; - .. o$ < k { - o+: [array??]; - } - * o; + * [array?? ... _ ->? <<..k>>]; } sample array k * { diff --git a/src/samarium/modules/string.sm b/src/samarium/modules/string.sm index 5cf13a2d..c1634b3a 100644 --- a/src/samarium/modules/string.sm +++ b/src/samarium/modules/string.sm @@ -31,48 +31,51 @@ center string length char? * { } starts_with string prefix * { - ? prefix$ > string$ { - * \; - } - ? prefix :: string { - * /; - } + ? prefix$ > string$ { * \; } + ? prefix :: string { * /; } * string<<..prefix$>> :: prefix; } ends_with string suffix * { - ? suffix$ > string$ { - * \; - } - ? suffix :: string { - * /; - } + ? suffix$ > string$ { * \; } + ? suffix :: string { * /; } * string<<-suffix$..>> :: suffix; } -split string delimiter? * { - delimiter <> " "; - ? delimiter ~~ ->? string { - * [string]; - } +#split_char string separator * { out: []; temp: ""; ... char ->? string { - ? char :: delimiter { + ? char :: separator { out+: [temp]; temp: ""; } ,, { temp+: char; } } - out+: [temp]; - * out; + * out + [temp]; } -capitalize string * { - ? string$ :: \ { - * string; +split string separator? * { + separator <> " "; + ? ~~ separator { "empty separator"!!!; } + ? separator ~~ ->? string { * [string]; } + ? separator$ :: / { + * #split_char(string, separator); } + out: []; + <=iter.find; + .. { + idx: find(string, separator); + ? idx :: -/ { <- } + out+: [string<<..idx>>]; + string: string<>; + } + * out + [string]; +} + +capitalize string * { + ? ~~ string { * string; } * to_upper(string<<\>>) + to_lower(string<>); } @@ -84,7 +87,7 @@ join iterable delimiter? * { delimiter <> ""; o: ""; ... e ->? iterable { - o+: e.to_string(); + o+: ""?!(e); o+: delimiter; } ? delimiter { @@ -107,7 +110,8 @@ strip_right string suffix * { * string; } -strip string chars * { +strip string chars? * { + chars <> " "; * strip_left(strip_right(string, chars), chars); } @@ -116,9 +120,8 @@ is_wrapped string chars * { } to_lower string * { - <-types.String; - ? string?! ::: String { - "invalid type: " + string?!.to_string()!!!; + ? string?! ::: <-types.String { + "invalid type: " + ""?!(string?!)!!!; } out: ""; ... char ->? string { @@ -132,9 +135,8 @@ to_lower string * { } to_upper string * { - <-types.String; - ? string?! ::: String { - "invalid type: " + string?!.to_string()!!!; + ? string?! ::: <-types.String { + "invalid type: " + ""?!(string?!)!!!; } out: ""; ... char ->? string { @@ -164,9 +166,7 @@ pad string length char? * { ? char$ > / { "char has to be of length 1"!!!; } - ? length < string$ { - * string; - } + ? length < string$ { * string; } * char ++ (length - string$); } @@ -227,47 +227,8 @@ wrap string wrapper * { * wrapper + string + wrapper; } -format string fields * { - <-types.Array, Table; - <-iter.enumerate; - ? fields?! :: Array { - fields: {{i -> v ... i, v ->? enumerate(fields)}}; - } - ? fields?! ::: Table { - "invalid type: " + fields?!.to_string()!!!; - } - ? ~~ fields { - * string; - } - new_string: ""; - index: \; - .. index < string$ { - char: string<>; - ? char ::: "$" { - new_string+: char; - index+: /; - } ,, ? string<> :: "$" { - new_string+: "$"; - index+: /\; - } ,, { - ... k ->? fields { - v: fields<>.to_string(); - k: k.to_string(); - ? string<> :: k { - new_string+: v; - index+: k$ + /; - <-; - } - } - } - } - * new_string; -} - replace string replacements count? * { count <> -/; - <-iter.all, find, zip; - <-types.String, Table; replace string from to count * { empty * { @@ -303,6 +264,7 @@ replace string replacements count? * { replace_substring count * { string_copy: string; out: ""; + find: <-iter.find; .. count && from ->? string_copy { i: find(string_copy, from); out+: string_copy<<..i>>; @@ -340,15 +302,24 @@ replace string replacements count? * { } } - ? replacements?! ::: Table { - "invalid type for replacements: " + String(replacements?!)!!!; + ? replacements?! ::: <-types.Table { + "invalid type for replacements: " + ""?!(replacements?!)!!!; } - replacements: {{k -> v ... k, v ->? zip([replacements.iterate(), replacements$])}}; - ? ~~ all([count, replacements]) { + replacements: {{k -> v ... k, v ->? replacements >< replacements$}}; + ? ~~ <-iter.all([count, replacements]) { * string; } - ... f, t ->? zip([replacements.iterate(), replacements$]) { + ... f, t ->? replacements >< replacements$ { string: replace(string, f, t, count); } * string; } + +ordinal n * { + 10: /\/\; + ords: {{/ -> "st", /\ -> "nd", // -> "rd"}}; + ? n --- (10 ++ 10) ~~ ->? <> && n --- 10 ->? ords { + * "$0$1" --- [n, ords<>]; + } + * "$0th" --- [n]; +} diff --git a/src/samarium/modules/types.sm b/src/samarium/modules/types.sm index 69c621aa..3a83ed4d 100644 --- a/src/samarium/modules/types.sm +++ b/src/samarium/modules/types.sm @@ -1,40 +1,31 @@ Array: []?!; Integer: \?!; -Iterator: [].iterate()?!; Null: (||)?!; Slice: <<>>?!; String: ""?!; Table: {{}}?!; +Zip: ("" >< "")?!; @ Boolean { - - create value * { + => value * { ? value?! :: ""?! { - <-string.to_lower; - 'value: {{"true" -> /, "false" -> \}}<>; + 'value: {{"true" -> /, "false" -> \}}<<<-string.to_lower(value)>>; } ,, { - 'value: value.to_bit(); + 'value: / ? value ,, \; } } - to_string * { - * "true" ? 'value ,, "false"; - } - - to_bit * { - * 'value; - } + ! * { * "true" ? 'value ,, "false"; } + ? * { * 'value; } - equals other * { + :: other * { * Boolean('value :: other.value); } - greater_than other * { - * Boolean('value > other.value); - } + > other * { * Boolean('value > other.value); } - add other * { + + other * { out: 'value + other.value; ? out > / { * out; @@ -43,7 +34,7 @@ Table: {{}}?!; } } - subtract other * { + - other * { out: 'value - other.value; ? out < \ { * out; @@ -52,63 +43,47 @@ Table: {{}}?!; } } - multiply other * { + ++ other * { * Boolean('value ++ other.value); } - divide other * { + -- other * { * Boolean('value -- other.value); } - power other * { + +++ other * { * Boolean('value +++ other.value); } - mod other * { + --- other * { * Boolean('value --- other.value); } - and other * { - * Boolean('value & other.value); - } - - xor other * { - * Boolean('value ^ other.value); - } - - or other * { - * Boolean('value | other.value); - } - - not * { - * Boolean(\ ? 'value ,, /); - } + & other * { * Boolean('value & other.value); } + ^ other * { * Boolean('value ^ other.value); } + | other * { * Boolean('value | other.value); } + ~ * { * Boolean(\ ? 'value ,, /); } } @ UUID4 { - - create * { - <-random.choices; - <-string.HEXDIGITS, join; - hex: join(choices(HEXDIGITS, /\\\\\)); + => * { + hex: <-string.join(<-random.choices(<-string.HEXDIGITS, /\\\\\)); hex<>: "4"; hex<>: "89ab"??; 'hex: hex; 'dec: Integer("x:" + hex); } - to_string * { - <-string.join; - * join( - [ - 'hex<<../\\\>>, - 'hex<>, - 'hex<>, - 'hex<>, - 'hex<> - ], "-" + ! * { + h: 'hex; + 8: /\\\; + 12: //\\; + 16: /\\\\; + 20: /\/\\; + * <-string.join( + [h<<..8>>, h<<8..12>>, h<<12..16>>, h<<16..20>>, h<<20..>>], + "-" ); } - } \ No newline at end of file diff --git a/src/samarium/objects.py b/src/samarium/objects.py deleted file mode 100644 index 8571d179..00000000 --- a/src/samarium/objects.py +++ /dev/null @@ -1,1144 +0,0 @@ -from __future__ import annotations - -import os - -from collections.abc import Iterable -from contextlib import contextmanager, suppress -from enum import Enum -from functools import lru_cache, wraps -from inspect import signature -from re import compile -from secrets import choice, randbelow -from types import FunctionType, GeneratorType -from typing import Any, Callable, IO, Iterator as Iter, cast - -from .exceptions import ( - NotDefinedError, - SamariumIOError, - SamariumSyntaxError, - SamariumTypeError, - SamariumValueError, -) -from .utils import get_callable_name, parse_integer, run_with_backup - - -I64_MAX = 9223372036854775807 - - -class MISSING: - def __getattr__(self, _): - raise SamariumValueError("cannot use the MISSING object") - - -class Class: - __slots__ = ("value",) - - def __init__(self, *args: Any): - with suppress(NotDefinedError): - self.sm_create(*args) - - def __bool__(self) -> bool: - return bool(self.sm_to_bit().value) - - def __str__(self) -> str: - return str(self.sm_to_string().value) - - def __iter__(self) -> Iter: - return iter(self.sm_iterate().value) - - def __contains__(self, element: Any) -> Integer: - return self.sm_has(element) - - def __call__(self, *args: Any) -> Any: - return self.sm_call(*args) - - def __hash__(self) -> int: - return self.sm_hash().value - - def __sub__(self, other: Class) -> Class: - return self.sm_subtract(other) - - def __isub__(self, other: Class) -> Class: - return self.sm_subtract_assign(other) - - def __add__(self, other: Class) -> Class: - return self.sm_add(other) - - def __iadd__(self, other: Class) -> Class: - return self.sm_add_assign(other) - - def __mul__(self, other: Class) -> Class: - return self.sm_multiply(other) - - def __imul__(self, other: Class) -> Class: - return self.sm_multiply_assign(other) - - def __floordiv__(self, other: Class) -> Class: - return self.sm_divide(other) - - def __ifloordiv__(self, other: Class) -> Class: - return self.sm_divide_assign(other) - - def __mod__(self, other: Class) -> Class: - return self.sm_mod(other) - - def __imod__(self, other: Class) -> Class: - return self.sm_mod_assign(other) - - def __pow__(self, other: Class) -> Class: - return self.sm_power(other) - - def __ipow__(self, other: Class) -> Class: - return self.sm_power_assign(other) - - def __and__(self, other: Class) -> Class: - return self.sm_and(other) - - def __iand__(self, other: Class) -> Class: - return self.sm_and_assign(other) - - def __or__(self, other: Class) -> Class: - return self.sm_or(other) - - def __ior__(self, other: Class) -> Class: - return self.sm_or_assign(other) - - def __xor__(self, other: Class) -> Class: - return self.sm_xor(other) - - def __ixor__(self, other: Class) -> Class: - return self.sm_xor_assign(other) - - def __neg__(self) -> Class: - return self.sm_negative() - - def __pos__(self) -> Class: - return self.sm_positive() - - def __invert__(self) -> Class: - return self.sm_not() - - def __getitem__(self, index: Integer | Slice) -> Any: - return self.sm_get_item(index) - - def __setitem__(self, index: Integer | Slice, value: Any): - return self.sm_set_item(index, value) - - def __eq__(self, other: Class) -> Integer: - return self.sm_equals(other) - - def __ne__(self, other: Class) -> Integer: - return run_with_backup( - self.sm_not_equals, lambda x: Int(not self.sm_equals(x)), other - ) - - def __lt__(self, other: Class) -> Integer: - return run_with_backup( - self.sm_less_than, - lambda x: Int(not (self.sm_greater_than(x) or self.sm_equals(x))), - other, - ) - - def __le__(self, other: Class) -> Integer: - return run_with_backup( - self.sm_less_than_or_equal, - lambda x: Int(not self.sm_greater_than(x)), - other, - ) - - def __gt__(self, other: Class) -> Integer: - return self.sm_greater_than(other) - - def __ge__(self, other: Class) -> Integer: - return run_with_backup( - self.sm_greater_than_or_equal, - lambda x: Int(self.sm_greater_than(x) or self.sm_equals(x)), - other, - ) - - @property - def id(self) -> String: - return String(f"{id(self):x}") - - @property - def type(self) -> Type: - return Type(type(self)) - - @property - def parent(self) -> Array | Type: - parents = type(self).__bases__ - if len(parents) == 1: - return Type(parents[0]) - return Array(map(Type, parents)) - - def sm_create(self, *args: Any): - raise NotDefinedError(self, "create") - - def sm_to_bit(self) -> Integer: - raise NotDefinedError(self, "to_bit") - - def sm_to_string(self) -> String: - return String(f"<{get_callable_name(type(self))}@{id(self):x}>") - - def sm_special(self) -> Any: - raise NotDefinedError(self, "special") - - def sm_has(self, element: Any) -> Integer: - raise NotDefinedError(self, "has") - - def sm_iterate(self) -> Array: - raise NotDefinedError(self, "iterate") - - def sm_call(self, *args: Any) -> Any: - raise NotDefinedError(self, "call") - - def sm_hash(self) -> Integer: - raise NotDefinedError(self, "hash") - - def sm_subtract(self, other: Class) -> Class: - raise NotDefinedError(self, "subtract") - - def sm_subtract_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "subtract_assign") - - def sm_add(self, other: Class) -> Class: - raise NotDefinedError(self, "add") - - def sm_add_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "add_assign") - - def sm_multiply(self, other: Class) -> Class: - raise NotDefinedError(self, "multiply") - - def sm_multiply_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "multiply_assign") - - def sm_divide(self, other: Class) -> Class: - raise NotDefinedError(self, "divide") - - def sm_divide_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "divide_assign") - - def sm_mod(self, other: Class) -> Class: - raise NotDefinedError(self, "mod") - - def sm_mod_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "mod_assign") - - def sm_power(self, other: Class) -> Class: - raise NotDefinedError(self, "power") - - def sm_power_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "power_assign") - - def sm_and(self, other: Class) -> Class: - raise NotDefinedError(self, "and") - - def sm_and_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "and_assign") - - def sm_or(self, other: Class) -> Class: - raise NotDefinedError(self, "or") - - def sm_or_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "or_assign") - - def sm_xor(self, other: Class) -> Class: - raise NotDefinedError(self, "xor") - - def sm_xor_assign(self, other: Class) -> Class: - raise NotDefinedError(self, "xor_assign") - - def sm_negative(self) -> Class: - raise NotDefinedError(self, "negative") - - def sm_positive(self) -> Class: - raise NotDefinedError(self, "positive") - - def sm_not(self) -> Class: - raise NotDefinedError(self, "not") - - def sm_get_item(self, index: Integer | Slice) -> Any: - raise NotDefinedError(self, "get_item") - - def sm_set_item(self, index: Integer | Slice, value: Any): - raise NotDefinedError(self, "set_item") - - def sm_equals(self, other: Class) -> Integer: - raise NotDefinedError(self, "equals") - - def sm_not_equals(self, other: Class) -> Integer: - raise NotDefinedError(self, "not_equals") - - def sm_less_than(self, other: Class) -> Integer: - raise NotDefinedError(self, "less_than") - - def sm_less_than_or_equal(self, other: Class) -> Integer: - raise NotDefinedError(self, "less_than_or_equal") - - def sm_greater_than(self, other: Class) -> Integer: - raise NotDefinedError(self, "greater_than") - - def sm_greater_than_or_equal(self, other: Class) -> Integer: - raise NotDefinedError(self, "greater_than_or_equal") - - def sm_cast(self): - raise NotDefinedError(self, "cast") - - def sm_random(self): - raise NotDefinedError(self, "random") - - -class Type(Class): - def sm_create(self, type_: type): - self.value = type_ - - def sm_equals(self, other: Type) -> Integer: - return Int(self.value == other.value) - - def sm_not_equals(self, other: Type) -> Integer: - return Int(self.value != other.value) - - def sm_to_string(self) -> String: - if self.value is FunctionType: - return String("Function") - return String(get_callable_name(self.value)) - - def sm_to_bit(self) -> Integer: - return Int(1) - - def sm_call(self, *args) -> Class: - if self.value is FunctionType: - raise SamariumTypeError("cannot instantiate a function") - if self.value is Module: - raise SamariumTypeError("cannot instantiate a module") - return self.value(*args) - - -class Null(Class): - def sm_create(self): - self.value = None - - def sm_to_string(self) -> String: - return String("null") - - def sm_hash(self) -> Integer: - return Int(hash(self.value)) - - def sm_to_bit(self) -> Integer: - return Int(0) - - def sm_equals(self, other: Null) -> Integer: - return Int(type(other) is Null) - - def sm_not_equals(self, other: Null) -> Integer: - return Int(type(other) is not Null) - - -null = Null() - - -class Slice(Class): - __slots__ = ("start", "stop", "step", "tup", "value", "range") - - def sm_create(self, start: Any = null, stop: Any = null, step: Any = null): - if step.value == 0: - raise SamariumValueError("step cannot be zero") - self.start = start - self.stop = stop - self.step = step - self.tup = start.value, stop.value, step.value - self.range = range( - self.tup[0] or 0, - I64_MAX if self.tup[1] is None else self.tup[1], - self.tup[2] or 1, - ) - self.value = slice(*self.tup) - - def sm_iterate(self) -> Iterator: - return Iterator(map(Int, self.range)) - - def sm_random(self) -> Integer: - if self.stop is None: - raise SamariumValueError( - "cannot generate a random value for slice with null stop" - ) - return Int(choice(self.range)) - - def sm_special(self) -> Integer: - return Int(len(self.range)) - - def sm_has(self, index: Integer) -> Integer: - return Int(index.value in self.range) - - def is_empty(self) -> bool: - return self.start == self.stop == self.step == null - - def sm_get_item(self, index: Integer) -> Integer: - return Int(self.range[index.value]) - - def sm_to_string(self) -> String: - start, stop, step = self.start, self.stop, self.step - if start is stop is step is null: - return String("<<>>") - string = "" - if start is not null: - string += get_repr(start) - if stop is step is null: - string += ".." - if stop is not null: - string += f"..{get_repr(stop)}" - if step is not null: - if stop is null: - string += ".." - string += f"..{get_repr(step)}" - return String(f"<<{string}>>") - - def sm_equals(self, other: Slice) -> Integer: - return Int(self.tup == other.tup) - - def sm_not_equals(self, other: Slice) -> Integer: - return Int(self.tup != other.tup) - - -class String(Class): - def __str__(self) -> str: - return self.value - - def sm_hash(self) -> Integer: - return Int(hash(self.value)) - - def sm_cast(self) -> Integer: - if len(self.value) != 1: - raise SamariumTypeError(f"cannot cast a string of length {len(self.value)}") - return Int(ord(self.value)) - - def sm_create(self, value: Any = ""): - self.value = str(value) - - def sm_has(self, element: String) -> Integer: - return Int(element.value in self.value) - - def sm_iterate(self) -> Iterator: - return Iterator(map(String, self.value)) - - def sm_random(self) -> String: - return String(choice(self.value)) - - def sm_special(self) -> Integer: - return Int(len(self.value)) - - def sm_to_bit(self) -> Integer: - return Int(self.value != "") - - def sm_to_string(self) -> String: - return self - - def sm_add(self, other: String) -> String: - return String(self.value + other.value) - - def sm_add_assign(self, other: String) -> String: - self = self.sm_add(other) - return self - - def sm_multiply(self, times: Integer) -> String: - return String(self.value * times.value) - - def sm_multiply_assign(self, times: Integer) -> String: - self = self.sm_multiply(times) - return self - - def sm_equals(self, other: String) -> Integer: - return Int(self.value == other.value) - - def sm_not_equals(self, other: String) -> Integer: - return Int(self.value != other.value) - - def sm_greater_than(self, other: String) -> Integer: - return Int(self.value > other.value) - - def sm_less_than(self, other: String) -> Integer: - return Int(self.value < other.value) - - def sm_greater_than_or_equal(self, other: String) -> Integer: - return Int(self.value >= other.value) - - def sm_less_than_or_equal(self, other: String) -> Integer: - return Int(self.value <= other.value) - - def sm_get_item(self, index: Integer | Slice) -> String: - return String(self.value[index.value]) - - def sm_set_item(self, index: Integer | Slice, value: String): - string = [*self.value] - string[index.value] = value.value - self.value = "".join(string) - - -class Integer(Class): - def __int__(self) -> int: - return self.value - - def sm_cast(self) -> String: - return String(chr(self.value)) - - def sm_hash(self) -> Integer: - return Int(hash(self.value)) - - def sm_create(self, value: Any = None): - if hasattr(value, "value"): - value = value.value - if isinstance(value, (int, bool, float)): - self.value = int(value) - elif value is None: - self.value = 0 - elif isinstance(value, str): - self.value = parse_integer(value) - else: - raise SamariumTypeError( - f"cannot cast {get_callable_name(type(value))} to Integer" - ) - - def sm_random(self) -> Integer: - v = self.value - if not v: - return self - elif v > 0: - return Int(randbelow(v)) - else: - return Int(-randbelow(v) - 1) - - def sm_to_bit(self) -> Integer: - return Int(self.value != 0) - - def sm_to_string(self) -> String: - return String(str(self.value)) - - def sm_add(self, other: Integer) -> Integer: - return Int(self.value + other.value) - - def sm_add_assign(self, other: Integer) -> Integer: - self = self.sm_add(other) - return self - - def sm_subtract(self, other: Integer) -> Integer: - return Int(self.value - other.value) - - def sm_subtract_assign(self, other: Integer) -> Integer: - self = self.sm_subtract(other) - return self - - def sm_multiply(self, other: Integer) -> Integer: - return Int(self.value * other.value) - - def sm_multiply_assign(self, other: Integer) -> Integer: - self = self.sm_multiply(other) - return self - - def sm_divide(self, other: Integer) -> Integer: - return Int(self.value // other.value) - - def sm_divide_assign(self, other: Integer) -> Integer: - self = self.sm_divide(other) - return self - - def sm_mod(self, other: Integer) -> Integer: - return Int(self.value % other.value) - - def sm_mod_assign(self, other: Integer) -> Integer: - self = self.sm_mod(other) - return self - - def sm_power(self, other: Integer) -> Integer: - return Int(self.value ** other.value) - - def sm_power_assign(self, other: Integer) -> Integer: - self = self.sm_power(other) - return self - - def sm_and(self, other: Integer) -> Integer: - return Int(self.value & other.value) - - def sm_and_assign(self, other: Integer) -> Integer: - self = self.sm_and(other) - return self - - def sm_or(self, other: Integer) -> Integer: - return Int(self.value | other.value) - - def sm_or_assign(self, other: Integer) -> Integer: - self = self.sm_or(other) - return self - - def sm_xor(self, other: Integer) -> Integer: - return Int(self.value ^ other.value) - - def sm_xor_assign(self, other: Integer) -> Integer: - self = self.sm_xor(other) - return self - - def sm_not(self) -> Integer: - return Int(~self.value) - - def sm_negative(self) -> Integer: - return Int(-self.value) - - def sm_positive(self) -> Integer: - return Int(+self.value) - - def sm_equals(self, other: Integer) -> Integer: - return Int(self.value == other.value) - - def sm_not_equals(self, other: Integer) -> Integer: - return Int(self.value != other.value) - - def sm_greater_than(self, other: Integer) -> Integer: - return Int(self.value > other.value) - - def sm_less_than(self, other: Integer) -> Integer: - return Int(self.value < other.value) - - def sm_greater_than_or_equal(self, other: Integer) -> Integer: - return Int(self.value >= other.value) - - def sm_less_than_or_equal(self, other: Integer) -> Integer: - return Int(self.value <= other.value) - - def sm_special(self) -> String: - return String(f"{self.value:b}") - - -Int = lru_cache(1024)(Integer) - - -class Table(Class): - def sm_create(self, value: Any = None): - if value is None: - self.value = {} - elif isinstance(value, Table): - self.value = value.value.copy() - elif isinstance(value, dict): - self.value = {verify_type(k): verify_type(v) for k, v in value.items()} - elif isinstance(value, Array): - arr = value.value - if all(isinstance(i, (String, Array)) and len(i.value) == 2 for i in arr): - table: dict[Class, Class] = {} - for e in arr: - if isinstance(e, String): - k, v = e.value - table[String(k)] = String(v) - else: - k, v = cast(Array, e) - table[k] = v - self.value = Table(table).value - else: - raise SamariumTypeError( - f"cannot cast {get_callable_name(type(value))} to Table" - ) - - def sm_special(self) -> Array: - return Array(self.value.values()) - - def sm_to_string(self) -> String: - return String( - "{{" - + ", ".join( - f"{get_repr(k)} -> {get_repr(v)}" for k, v in self.value.items() - ) - + "}}" - ) - - def sm_to_bit(self) -> Integer: - return Int(self.value != {}) - - def sm_get_item(self, key: Any) -> Any: - try: - return self.value[key] - except KeyError: - raise SamariumValueError(f"key not found: {key}") - - def sm_set_item(self, key: Any, value: Any): - self.value[key] = value - - def sm_iterate(self) -> Iterator: - return Iterator(self.value.keys()) - - def sm_random(self) -> Any: - if not self.value: - raise SamariumValueError("table is empty") - return choice([*self.value.keys()]) - - def sm_has(self, element: Any) -> Integer: - return Int(element in self.value) - - def sm_equals(self, other: Table) -> Integer: - return Int(self.value == other.value) - - def sm_not_equals(self, other: Table) -> Integer: - return Int(self.value == other.value) - - def sm_add(self, other: Table) -> Table: - return Table(self.value | other.value) - - def sm_add_assign(self, other: Table) -> Table: - self.value.update(other.value) - return self - - def sm_subtract(self, other: Class) -> Table: - c = self.value.copy() - try: - del c[other] - except KeyError: - raise SamariumValueError(f"key not found: {other}") - return Table(c) - - def sm_subtract_assign(self, other: Class) -> Table: - try: - del self.value[other] - except KeyError: - raise SamariumValueError(f"key not found: {other}") - return self - - -class Array(Class): - def sm_create(self, value: Any = None): - if value is None: - self.value = [] - elif isinstance(value, Array): - self.value = value.value.copy() - elif isinstance(value, String): - self.value = Array(map(String, value.value)).value - elif isinstance(value, Table): - self.value = Array(map(Array, value.value.items())).value - elif isinstance(value, Iterator): - self.value = Array(list(value.value)).value - elif isinstance(value, Slice) and value.sm_special() is null: - raise SamariumTypeError("cannot convert an infinite slice to Array") - elif isinstance(value, Iterable): - self.value = [*map(verify_type, value)] - else: - raise SamariumTypeError( - f"cannot cast {get_callable_name(type(value))} to Array" - ) - - def sm_special(self) -> Integer: - return Int(len(self.value)) - - def sm_to_string(self) -> String: - return String(f"[{', '.join(map(get_repr, self.value))}]") - - def sm_to_bit(self) -> Integer: - return Int(self.value != []) - - def __iter__(self) -> Iter: - yield from self.value - - def sm_iterate(self) -> Iterator: - return Iterator(self) - - def sm_random(self) -> Any: - if not self.value: - raise SamariumValueError("array is empty") - return choice(self.value) - - def sm_has(self, element: Any) -> Integer: - return Int(element in self.value) - - def sm_equals(self, other: Array) -> Integer: - return Int(self.value == other.value) - - def sm_not_equals(self, other: Array) -> Integer: - return Int(self.value != other.value) - - def sm_greater_than(self, other: Array) -> Integer: - return Int(cmp(self.value, other.value) == 1) - - def sm_less_than(self, other: Array) -> Integer: - return Int(cmp(self.value, other.value) == -1) - - def sm_greater_than_or_equal(self, other: Array) -> Integer: - return Int(cmp(self.value, other.value) != -1) - - def sm_less_than_or_equal(self, other: Array) -> Integer: - return Int(cmp(self.value, other.value) != 1) - - def sm_get_item(self, index: Integer | Slice) -> Any: - if isinstance(index, Integer): - return self.value[index.value] - return Array(self.value[index.value]) - - def sm_set_item(self, index: Integer | Slice, value: Any): - self.value[index.value] = value - - def sm_add(self, other: Array) -> Array: - return Array(self.value + other.value) - - def sm_add_assign(self, other: Array) -> Array: - self.value += other.value - return self - - def sm_subtract(self, other: Array | Integer) -> Array: - new_array = self.value.copy() - if isinstance(other, Array): - for i in other: - new_array.remove(i) - elif isinstance(other, Integer): - new_array.pop(other.value) - else: - raise SamariumTypeError(type(other).__name__) - return Array(new_array) - - def sm_subtract_assign(self, other: Array | Integer) -> Array: - if isinstance(other, Array): - for i in other: - self.value.remove(i) - elif isinstance(other, Integer): - self.value.pop(other.value) - else: - raise SamariumTypeError(type(other).__name__) - return self - - def sm_multiply(self, other: Integer) -> Array: - return Array(self.value * other.value) - - def sm_multiply_assign(self, other: Integer) -> Array: - self.value *= other.value - return self - - -class Mode(Enum): - READ = "r" - WRITE = "w" - READ_WRITE = "r+" - APPEND = "a" - - -class FileManager: - @staticmethod - def create(path: String): - open(path.value, "x").close() - return null - - @staticmethod - def open(path: String | Integer, mode: Mode, *, binary: bool = False) -> File: - if isinstance(path, Integer) and mode is Mode.READ_WRITE: - raise SamariumIOError( - "cannot open a standard stream in a read & write mode" - ) - f = open(path.value, mode.value + "b" * binary) - return File(f, mode.name, path.value, binary) - - @staticmethod - def open_binary(path: String, mode: Mode) -> File: - return FileManager.open(path, mode, binary=True) - - @staticmethod - def quick( - path: String | File | Integer, - mode: Mode, - *, - data: String | Array | None = None, - binary: bool = False, - ) -> String | Array | Null: - if isinstance(path, String): - with open(path.value, mode.value + "b" * binary) as f: - if mode is Mode.READ: - if binary: - return Array(map(Int, f.read())) - return String(f.read()) - if data is None: - raise SamariumIOError("missing data") - if isinstance(data, Array): - f.write(b"".join(x.value.to_bytes(1, "big") for x in data.value)) - else: - f.write(data.value) - elif isinstance(path, Integer): - if mode in {Mode.APPEND, Mode.WRITE}: - fd = cast(int, path.value) - os.write(fd, str(data).encode()) - else: - raise SamariumIOError( - "reading from file descriptors is " - "not supported for quick operations" - ) - else: - file = path - if mode is Mode.READ: - return file.load() - if data is not None: - file.save(data) - else: - raise SamariumIOError("missing data") - return null - - -class File(Class): - __slots__ = ("binary", "mode", "path", "value") - - def sm_create(self, file: IO, mode: str, path: str, binary: bool): - self.binary = binary - self.mode = mode - self.path = path - self.value = file - - def sm_to_string(self) -> String: - return String(f"File(path:{self.path}, mode:{self.mode})") - - def sm_not(self): - self.value.close() - return null - - def sm_get_item(self, index: Integer | Slice) -> Array | String | Integer | Null: - if isinstance(index, Slice): - if index.is_empty(): - return Int(self.value.tell()) - if isinstance(index.step, Integer): - raise SamariumIOError("cannot use step") - if isinstance(index.start, Integer): - if not isinstance(index.stop, Integer): - return self.load(index.start) - current_position = self.value.tell() - self.value.seek(index.start.value) - data = self.value.read(index.stop.value - index.start.value) - self.value.seek(current_position) - if self.binary: - if isinstance(data, bytes): - data = [*data] - return Array(map(Int, data)) - return String(data) - return self[Slice(Int(0), slice.stop, slice.step)] - else: - self.value.seek(index.value) - return null - - def load(self, bytes_: Integer | None = None) -> String | Array: - if bytes_ is None: - bytes_ = Int(-1) - val = self.value.read(cast(Integer, bytes_).value) - if self.binary: - return Array(map(Int, val)) - return String(val) - - def save(self, data: String | Array): - if (self.binary and isinstance(data, String)) or ( - not self.binary and isinstance(data, Array) - ): - raise SamariumTypeError(type(data).__name__) - if isinstance(data, Array): - self.value.write(b"".join(x.value.to_bytes(1, "big") for x in data.value)) - else: - self.value.write(data.value) - return null - - -class Module: - __slots__ = ("name", "objects") - - def __init__(self, name: str, objects: dict[str, Class]): - self.name = name - self.objects = objects - - def __str__(self) -> str: - return f"module '{self.name}'" - - def __getattr__(self, key: str) -> Class: - return self.objects[key] - - @property - def type(self) -> Type: - return Type(type(self)) - - -class Enum_(Class): - __slots__ = ("name", "members") - - def sm_create(self, globals: dict[str, Any], *values_: str) -> None: - if any(isinstance(i, Class) for i in values_): - raise SamariumTypeError("enums cannot be constructed from Type") - name, *values = values_ - self.name = name.removeprefix("sm_") - self.members: dict[str, Class] = {} - - # Empty enum case - if len(values) == 1 and not values[0]: - raise SamariumValueError("enums must have at least 1 member") - - i = 0 - for v in values: - if not v: - continue - eqs = v.count("=") - if eqs >= 2: - raise SamariumSyntaxError("invalid expression") - name, value = v.split("=") if eqs == 1 else (v, "") - if not name.isidentifier(): - raise SamariumValueError("enum members must be identifiers") - if eqs == 1: - self.members[name] = eval(value, globals) - else: - self.members[v] = Int(i) - i += 1 - - def sm_to_string(self) -> String: - return String(f"Enum({self.name})") - - def __getattr__(self, name: str) -> Class: - try: - return self.members[name] - except KeyError: - raise AttributeError(f"'{self.name}''{name}'") - - def __setattr__(self, name: str, value: Any) -> None: - if name.startswith("sm_"): - raise SamariumTypeError("enum members cannot be modified") - object.__setattr__(self, name, value) - - -class Iterator(Class): - __slots__ = ("value", "length") - - def sm_create(self, value: Class) -> None: - if not isinstance(value, Iterable): - raise SamariumTypeError("cannot create an Iterator from a non-iterable") - self.value = iter(value) - try: - self.length = Int(len(value.value)) - except (TypeError, AttributeError): - self.length = null - - def __iter__(self) -> Iter: - return self.value - - def __next__(self) -> Class: - return next(self.value) - - def sm_cast(self) -> Integer | Null: - return self.length - - def sm_iterate(self) -> Iterator: - return self - - def sm_special(self) -> Class: - return next(self) - - -def class_attributes(cls): - cls.argc = Int(len(signature(cls.sm_create).parameters)) - cls.type = Type(Type) - parents = cls.__bases__ - cls.parent = Type(parents[0]) if len(parents) == 1 else Array(map(Type, parents)) - return cls - - -def cmp(arr1: list[Any], arr2: list[Any]) -> int: - for a, b in zip(arr1, arr2): - if a == b: - continue - return 1 if a > b else -1 - len1 = len(arr1) - len2 = len(arr2) - if len1 != len2: - return 1 if len1 > len2 else -1 - return 0 - - -def get_repr(obj: Class | Callable | Module) -> str: - if isinstance(obj, String): - return f'"{obj}"' - return str(obj.sm_to_string()) - - -def mkslice(start: Any = MISSING, stop: Any = MISSING, step: Any = MISSING) -> Class: - if stop is step is MISSING: - if start is None: - return Slice(null, null, null) - return start - missing_none = {MISSING, None} - start = null if start in missing_none else start - stop = null if stop in missing_none else stop - step = null if step in missing_none else step - return Slice(start, stop, step) - - -def t(obj: Any = None) -> Any: - return obj - - -def verify_type(obj: Any, *args) -> Class | Callable | Module: - if args: - for i in [obj, *args]: - verify_type(i) - return null - elif isinstance(obj, type): - return Type(obj) - elif isinstance(obj, (Class, Callable, Module)): - return obj - elif isinstance(obj, tuple): - return Array(obj) - elif obj is None: - return null - elif isinstance(obj, bool): - return Int(obj) - elif isinstance(obj, GeneratorType): - return Iterator(obj) - else: - raise SamariumTypeError(f"unknown type: {type(obj).__name__}") - - -@contextmanager -def modify(func: Callable, args: list[Any], argc: int): - flag = func.__code__.co_flags - if flag & 4 == 0: - yield func, args - return - x = argc - 1 - args = [*args[:x], Array(args[x:])] - func.__code__ = func.__code__.replace(co_flags=flag - 4, co_argcount=argc) - args *= argc > 0 - yield func, args - func.__code__ = func.__code__.replace(co_flags=flag, co_argcount=argc - 1) - - -MISSING_ARGS_PATTERN = compile( - r"\w+\(\) takes exactly one argument \(0 given\)" - r"|\w+\(\) missing (\d+) required positional argument" -) -TOO_MANY_ARGS_PATTERN = compile( - r"\w+\(\) takes (\d+) positional arguments? but (\d+) (?:was|were) given" -) - - -def function(func: Callable): - @wraps(func) - def wrapper(*args): - args = [*map(verify_type, args)] - with modify(func, args, argc) as (f, args): - try: - result = verify_type(f(*args)) - except TypeError as e: - errmsg = str(e) - if "missing 1 required positional argument: 'self'" in errmsg: - raise SamariumTypeError("missing instance") - missing_args = MISSING_ARGS_PATTERN.search(errmsg) - if missing_args: - given = argc - (int(missing_args.group(1)) or 1) - raise SamariumTypeError(f"not enough arguments ({given}/{argc})") - too_many_args = TOO_MANY_ARGS_PATTERN.search(errmsg) - if too_many_args: - raise SamariumTypeError( - f"too many arguments ({too_many_args.group(2)}/{argc})" - ) - raise e - if isinstance(result, (Class, Callable, Module)): - return result - raise SamariumTypeError(f"invalid return type: {type(result).__name__}") - - argc = len(signature(func).parameters) - - wrapper.sm_to_string = lambda: String(get_callable_name(func)) - wrapper.sm_special = lambda: Int(argc) - wrapper.argc = Int(argc) - wrapper.parent = Type(Class) - wrapper.type = Type(FunctionType) - - return wrapper diff --git a/src/samarium/python.py b/src/samarium/python.py new file mode 100644 index 00000000..4c2bcf63 --- /dev/null +++ b/src/samarium/python.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from enum import Enum as PyEnum +from functools import wraps +from io import BufferedIOBase, IOBase +from types import FunctionType +from typing import Iterable as PyIterable + +from samarium.classes import ( + NULL, + Array, + Attrs, + Enum, + File, + Int, + Integer, + Iterator, + Mode, + Null, + Slice, + String, + Table, + Zip, +) + + +class SliceRange: + def __init__(self, slice_: Slice) -> None: + self._slice = slice_ + + @property + def slice(self) -> slice: + return self._slice.val + + @property + def range(self) -> range: + return self._slice.range + + +def to_python(obj: Attrs) -> object: + if isinstance(obj, (String, Integer, Zip, File)): + return obj.val + elif isinstance(obj, Null): + return None + elif isinstance(obj, Array): + return [to_python(i) for i in obj.val] + elif isinstance(obj, Table): + return {to_python(k): to_python(v) for k, v in obj.val.items()} + elif isinstance(obj, Slice): + return SliceRange(obj) + elif isinstance(obj, Enum): + o = {k.removeprefix("sm_"): to_python(v) for k, v in obj.members.items()} + return PyEnum(obj.name, o) + elif isinstance(obj, Iterator): + return map(to_python, obj) + raise TypeError(f"Conversion for type {type(obj).__name__!r} not found") + + +def to_samarium(obj: object) -> Attrs: + if isinstance(obj, (int, bool, float)): + return Int(obj) + elif isinstance(obj, str): + return String(obj) + elif obj is None: + return NULL + elif isinstance(obj, (list, tuple, set)): + return Array([to_samarium(i) for i in obj]) + elif isinstance(obj, dict): + return Table({to_samarium(k): to_samarium(v) for k, v in obj.items()}) + elif isinstance(obj, (range, slice)): + return Slice( + to_samarium(None if obj.start == 0 else obj.start), + to_samarium(obj.stop), + to_samarium(None if obj.step == 1 else obj.step), + ) + elif isinstance(obj, SliceRange): + return obj._slice + elif isinstance(obj, IOBase): + return File(obj, Mode(obj.mode).name, obj.name, binary=isinstance(obj, BufferedIOBase)) # type: ignore + elif isinstance(obj, zip): + return Zip(*obj) + elif isinstance(obj, type) and issubclass(obj, PyEnum): + o = {f"sm_{k}": to_samarium(v) for k, v in obj.__members__.items()} + return Enum(f"sm_{obj.__name__}", **o) + elif isinstance(obj, PyEnum): + return to_samarium(obj.value) + elif isinstance(obj, PyIterable): + return Iterator(obj) + raise TypeError(f"Conversion for type {type(obj).__name__!r} not found") + + +def export(func): + """Wraps a Python function to be used in Samarium""" + + @wraps(func) + def wrapper(*_args): + args = map(to_python, _args) + return to_samarium(func(*args)) + + if not isinstance(func, FunctionType): + raise TypeError( + f"cannot export a non-function type {type(func).__name__!r}" + ) + setattr(wrapper, f"__export_{wrapper}", True) + + return wrapper diff --git a/src/samarium/runtime.py b/src/samarium/runtime.py index eabeccb6..d2ba1b89 100644 --- a/src/samarium/runtime.py +++ b/src/samarium/runtime.py @@ -1,3 +1,2 @@ class Runtime: - import_level = 0 quit_on_error = True diff --git a/src/samarium/shell.py b/src/samarium/shell.py index 6e0aceae..1307ae77 100644 --- a/src/samarium/shell.py +++ b/src/samarium/shell.py @@ -1,13 +1,12 @@ -from dahlia import dahlia, dprint +import readline # used by input() to provide elaborate line editing & history features from .core import run from .tokenizer import tokenize from .transpiler import Registry -from .utils import match_brackets, __version__ +from .utils import __version__, match_brackets - -IN = dahlia("&3==> ") -INDENT = dahlia("&3 > ") +IN = "--> " +INDENT = " > " def read_statement() -> str: @@ -24,13 +23,13 @@ def read_statement() -> str: statement = statement[:-1] -def run_shell(debug: bool): - dprint(f"&3Samarium {__version__}") - MAIN = Registry(globals()) +def run_shell(*, debug: bool) -> None: + print(f"Samarium {__version__}" + " [DEBUG]" * debug) + main = Registry(globals()) while True: try: - run(read_statement(), MAIN, debug, load_template=False, quit_on_error=False) - MAIN.output *= 0 + run(read_statement(), main, debug, load_template=False) + main.output = "" except KeyboardInterrupt: print() except EOFError: diff --git a/src/samarium/template.py b/src/samarium/template.py index 4f3beac7..f3291b07 100644 --- a/src/samarium/template.py +++ b/src/samarium/template.py @@ -1,6 +1,6 @@ -from inspect import signature -import sys import os +import sys + STDOUT = sys.stdout sys.stdout = open(os.devnull, "w") {{ CODE }} @@ -9,12 +9,12 @@ argc = entry.argc is_class = isinstance(entry, type) if is_class: - argc.value -= 1 + argc -= 1 if not argc: ex = entry() - elif argc.value == 1: + elif argc == 1: ex = entry(Array(map(String, sys.argv[1:]))) else: raise SamariumSyntaxError("entry function should take 0 or 1 arguments") if not is_class: - sys.exit(ex.value) + sys.exit(ex.val) diff --git a/src/samarium/tokenizer.py b/src/samarium/tokenizer.py index 13bf98dc..8851d30b 100644 --- a/src/samarium/tokenizer.py +++ b/src/samarium/tokenizer.py @@ -1,137 +1,38 @@ -from contextlib import suppress -from string import ascii_letters, digits -from typing import Union +from __future__ import annotations -from . import handlers -from .exceptions import SamariumSyntaxError, handle_exception -from .tokens import Token - -Tokenlike = Union[Token, str, int] - -CHARSET = ascii_letters + digits + "_" - -MULTISEMANTIC = { - "+": handlers.plus, - "-": handlers.minus, - ":": handlers.colon, - "<": handlers.less, - ">": handlers.greater, - "=": handlers.equal, - ".": handlers.dot, - "?": handlers.question, - "!": handlers.exclamation, - "&": handlers.ampersand, - "|": handlers.pipe, - "~": handlers.tilde, - ",": handlers.comma, - "{": handlers.open_brace, - "}": handlers.close_brace, - "^": handlers.caret, - "#": handlers.hash_, - "*": handlers.asterisk, - "%": handlers.percent, - "@": handlers.at, -} - - -def tokenize(program: str) -> list[Tokenlike]: - - comment = False - scroller = handlers.Scroller(exclude_backticks(program)) - string = False - temp = "" - tokens: list[Tokenlike] = [] - - while scroller.program: - - if comment: - if scroller.pointer == "\n": - comment = False - - # String submitting - elif scroller.pointer == '"' and is_safely_escaped(temp): - if not string and temp: - tokens.append(temp) - temp = "" - temp += '"' - string = not string - if not string: - tokens.append(temp) - temp = "" +import re +import sys +from typing import Union, cast - # String content and name handling - elif scroller.pointer in CHARSET or string: - temp += scroller.pointer - - # Namespace submitting - elif temp and scroller.pointer not in CHARSET and not string: - tokens.append(temp) - temp = "" - continue - - # Multisemantic token handling - elif scroller.pointer in MULTISEMANTIC: - if not (out := MULTISEMANTIC[scroller.pointer](scroller)): - handle_exception( - SamariumSyntaxError(f"invalid token: {scroller.pointer}") - ) - if out == Token.COMMENT: - comment = True - scroller.shift(2) - continue - tokens.append(out) - scroller.shift(len(out.value)) - continue - - # Number handling - elif scroller.pointer in "/\\": - number, length = tokenize_number(scroller) - tokens.append(number) - scroller.shift(length) - continue - - else: - with suppress(ValueError): - tokens.append(Token(scroller.pointer)) - - scroller.shift() - - return exclude_comments(tokens) - - -def tokenize_number(scroller: handlers.Scroller) -> tuple[int, int]: - temp = "" - for char in scroller.program: - if char not in "/\\": - break - temp += char - temp = temp.replace("/", "1").replace("\\", "0") - return int(temp, 2), len(temp) +from crossandra import Crossandra, CrossandraError, Rule, common +from .exceptions import SamariumSyntaxError, handle_exception +from .tokens import Token -def is_safely_escaped(string: str) -> bool: - return (len(string) - len(string.rstrip("\\"))) % 2 == 0 +def to_int(string: str) -> int: + return int(string.replace("/", "1").replace("\\", "0"), 2) -def exclude_backticks(program: str) -> str: - out = "" - skip = False - for i, c in enumerate(program): - if c == '"' and is_safely_escaped(program[:i]): - skip = not skip - if c == "`" and skip or c != "`": - out += c - return out +Tokenlike = Union[Token, str, int] -def exclude_comments(tokens: list[Tokenlike]) -> list[Tokenlike]: - out = [] - ignore = False - for token in tokens: - if token == Token.COMMENT_OPEN: - ignore = True - elif token == Token.COMMENT_CLOSE: - ignore = False - elif not ignore: - out.append(token) - return out +crossandra = Crossandra( + Token, + ignore_whitespace=True, + ignored_characters="`", + rules=[ + common.DOUBLE_QUOTED_STRING, + Rule(r"==[^\n]*", False, re.M | re.S), + Rule(r"==<.*>==", False, re.M | re.S), + Rule(r"(?:\\|/)+", to_int), + Rule(r"\w+"), + ], +) + + +def tokenize(code: str) -> list[Tokenlike]: + try: + return cast(list[Tokenlike], crossandra.tokenize(code)) + except CrossandraError as e: + handle_exception(SamariumSyntaxError(str(e))) + sys.exit() diff --git a/src/samarium/tokens.py b/src/samarium/tokens.py index 43605b40..17b523e7 100644 --- a/src/samarium/tokens.py +++ b/src/samarium/tokens.py @@ -51,16 +51,12 @@ class Token(Enum): FOR = "..." FROM = "<-" IF = "?" + IMPORT = "<=" THROW = "!!!" TO = "->" TRY = "??" WHILE = ".." - # Comments - COMMENT = "==" - COMMENT_OPEN = "==<" - COMMENT_CLOSE = ">==" - # OOP / Functions CLASS = "@" DEFAULT = "<>" @@ -109,7 +105,7 @@ class Token(Enum): END = ";" SEP = "," SLEEP = ",.," - STRING = '"' + ZIP = "><" FILE_IO_TOKENS = [ diff --git a/src/samarium/transpiler.py b/src/samarium/transpiler.py index 9418fbb2..407b7cac 100644 --- a/src/samarium/transpiler.py +++ b/src/samarium/transpiler.py @@ -1,9 +1,10 @@ from __future__ import annotations + from contextlib import suppress from enum import Enum from typing import Any, cast -from .exceptions import handle_exception, SamariumSyntaxError +from .exceptions import SamariumSyntaxError, handle_exception from .tokenizer import Tokenlike from .tokens import FILE_IO_TOKENS, Token from .utils import match_brackets @@ -17,19 +18,12 @@ def groupnames(array: list[str]) -> list[str]: if item == " for ": grouped[-1] = f"*{grouped[-1]}" elif item == " if ": - grouped[-1] += "=MISSING()" + grouped[-1] += "=MISSING" else: grouped.append(item) return grouped -def find_next(tokens: list[Tokenlike], token: Token, *, after: int = 0) -> int: - for i, t in enumerate(tokens[after:]): - if t == token: - return i + after - return -1 - - def indent(levels: int) -> str: return " " * levels * 4 @@ -42,6 +36,13 @@ def throw_syntax(message: str) -> None: handle_exception(SamariumSyntaxError(message)) +def transform_special(op: str, scope: Scope) -> str: + special = scope.current == "function" and scope.parent == "class" + if special and op in SPECIAL_METHOD_MAPPING: + return f"__{SPECIAL_METHOD_MAPPING[op]}__" + return op + + class Scope: def __init__(self) -> None: self._scope: list[str] = [] @@ -50,7 +51,10 @@ def enter(self, name: str) -> None: self._scope.append(name) def exit(self) -> None: - self._scope.pop() + try: + self._scope.pop() + except IndexError: + throw_syntax("invalid syntax (failed scope resolution)") def _get(self, index: int) -> str | None: try: @@ -73,7 +77,6 @@ class Switch(Enum): FUNCTION = 2 IMPORT = 3 BUILTIN = 4 - SLICE = 5 class Registry: @@ -115,6 +118,7 @@ class Group: Token.BXOR, Token.BNOT, Token.IN, + Token.ZIP, } brackets = { Token.BRACKET_OPEN, @@ -128,17 +132,18 @@ class Group: } functions = {Token.FUNCTION, Token.YIELD, Token.ENTRY, Token.DEFAULT} multisemantic = { - Token.FROM, Token.TO, Token.CATCH, Token.WHILE, Token.CLASS, Token.TRY, + Token.FROM, } control_flow = {Token.IF, Token.ELSE, Token.FOR} core = { Token.ASSIGN, Token.END, + Token.IMPORT, Token.SEP, Token.ATTR, Token.INSTANCE, @@ -180,6 +185,7 @@ class Group: Token.BXOR: "^", Token.BNOT: "~", Token.IN: " in ", + Token.ZIP: "@", } BRACKET_MAPPING = { @@ -192,11 +198,11 @@ class Group: } METHOD_MAPPING = { - Token.SPECIAL: ".sm_special()", - Token.CAST: ".sm_cast()", - Token.HASH: ".sm_hash()", - Token.TYPE: ".type", - Token.PARENT: ".parent", + Token.SPECIAL: ".special()", + Token.CAST: ".cast()", + Token.HASH: ".hash()", + Token.TYPE: ".type()", + Token.PARENT: ".parent()", } CONTROL_FLOW_TOKENS = { @@ -233,10 +239,45 @@ class Group: Token.PAREN_OPEN, Token.TABLE_OPEN, Token.IN, + Token.IF, + Token.WHILE, + *Group.operators, } FILE_OPEN_KEYWORDS = {"READ", "WRITE", "READ_WRITE", "APPEND"} +SPECIAL_METHOD_MAPPING = { + "+": "add", + "*": "mul", + "//": "floordiv", + "**": "pow", + "%": "mod", + "-": "sub", + "entry ": "entry", + "~": "invert", + "!=": "ne", + "==": "eq", + ">": "gt", + "<": "lt", + ">=": "ge", + "<=": "le", + " in ": "contains", + ".hash()": "hsh", + ".special()": "special", + "try": "random", + ".cast()": "cast", + "if ": "bit", + "!": "string", # + "&": "and", + "|": "or", + "^": "xor", + "+sm__": "pos", + "-sm__": "neg", + "@": "matmul", + ".__getitem__(mkslice(t()))": "getitem", + ".__setitem__(mkslice(t()),": "setitem", +} + class Transpiler: def __init__(self, tokens: list[Tokenlike], registry: Registry) -> None: @@ -245,6 +286,7 @@ def __init__(self, tokens: list[Tokenlike], registry: Registry) -> None: self._file_token: Token | None = None self._indent = 0 self._index = 0 + self._inline_counter = 0 self._line: list[str] = [] self._line_tokens: list[Tokenlike] = [] self._private = False @@ -273,15 +315,20 @@ def transpile(self) -> Registry: self._reg.output = self._code return self._reg - def _submit_line(self) -> None: + def _find_next(self, token: Token) -> int: + for i, t in enumerate(self._tokens[self._index :]): + if t is token: + return i + self._index + return -1 + def _submit_line(self) -> None: # Special cases if self._reg[Switch.IMPORT]: self._line.append("', Registry(globals()))") self._reg[Switch.IMPORT] = False if len(self._line) > 1 and self._line[-2] == "=": - self._line.insert(-1, "null") + self._line.insert(-1, "NULL") if self._file_token: self._file_io() @@ -347,7 +394,7 @@ def _file_io(self) -> None: def _operators(self, token: Token) -> None: prev_token = self._tokens[self._index - 1] if token in {Token.NOT, Token.BNOT} and prev_token in Group.operators: - self._line.append("null") + self._line.append("NULL") elif token is Token.IN and prev_token is Token.NOT: pass elif ( @@ -363,12 +410,15 @@ def _operators(self, token: Token) -> None: } or is_first_token(self._line) ) and token not in {Token.ADD, Token.SUB, Token.NOT, Token.BNOT}: - self._line.append("null") + self._line.append("NULL") self._line.append(OPERATOR_MAPPING[token]) def _brackets(self, token: Token) -> None: if token is Token.BRACE_OPEN: - if self._line_tokens[0] in CONTROL_FLOW_TOKENS: + if ( + self._line_tokens[0] in CONTROL_FLOW_TOKENS + and self._line_tokens[1] is not Token.FUNCTION + ): self._scope.enter("control_flow") # Implicit infinite loop @@ -376,12 +426,12 @@ def _brackets(self, token: Token) -> None: self._line.append("True") if self._line_tokens[-2] in Group.operators: - self._line.append("null") + self._line.append("NULL") - # Inheriting from Class if no parent is specified + # Getting UserAttrs if self._reg[Switch.CLASS_DEF]: if not isinstance(self._line_tokens[-3], str): - self._line.append("(Class)") + self._line.append("(UserAttrs)") self._reg[Switch.CLASS_DEF] = False self._indent += 1 @@ -397,7 +447,7 @@ def _brackets(self, token: Token) -> None: self._class_indent.pop() if self._scope.current == "enum": - self._line.append('""")') + self._line.append(")") if (self._processed_tokens or self._line_tokens)[-1] is Token.BRACE_OPEN: # Managing empty bodies @@ -416,9 +466,9 @@ def _brackets(self, token: Token) -> None: else: prev = self._tokens[self._index - 1] if token is Token.TABLE_CLOSE and prev is Token.TO: - self._line.append("null") - if token is Token.PAREN_CLOSE and prev in Group.operators: - self._line.append("null") + self._line.append("NULL") + if token is Token.PAREN_CLOSE and prev in Group.operators | {Token.ELSE}: + self._line.append("NULL") self._line.append(BRACKET_MAPPING[token]) return @@ -427,7 +477,7 @@ def _brackets(self, token: Token) -> None: def _functions(self, token: Token) -> None: if token is Token.FUNCTION: # Import all - if self._line_tokens[0] is Token.FROM: + if self._line_tokens[0] is Token.IMPORT: self._line.append("*") return @@ -440,31 +490,52 @@ def _functions(self, token: Token) -> None: # Function definition self._scope.enter("function") indentation = self._line[:indented] + name = self._line[indented] + method = self._reg[Switch.CLASS] and self._scope.parent == "class" + static = self._line[-2:] == ["~", "self"] + if static and not method: + throw_syntax("cannot create a static method outside a class") + if static: + self._line = self._line[:-2] + if name == "NULL": + indented += 1 + name = self._line[indented] + if name in "+-" and self._line[indented + 1] == "sm__": + name += "sm__" + indented += 1 + if name in {"(", ".__getitem__(", ".__setitem__("}: + indented += 1 + name += self._line[indented] + if self._line[indented + 1] in {")))", "))"}: + indented += 1 + name += self._line[indented] + if self._line[indented + 1] == ",": + self._slice_assign = False + indented += 1 + name += self._line[indented] self._line = [ *indentation, "@function\n", *indentation, "def ", - self._line[indented], + transform_special(name, self._scope), "(", ",".join( - groupnames( # Making varargs and optionals work - ( - # Adding the self parameter for class methods - ["self"] - if self._reg[Switch.CLASS] and self._scope.parent == "class" - else [] - ) + # Making varargs and optionals work + groupnames( + # Adding the self parameter for class methods + (["self"] if method and not static else []) + self._line[indented + 1 :] ) ), ")", ] + if static: + self._line.insert(0, "@staticmethod\n") + self._line.insert(0, indentation[0] if indentation else "") elif token is Token.DEFAULT: self._line.append( - " = {0} if not isinstance({0}, MISSING) else ".format( - "".join(self._line).strip() - ) + " = {0} if {0} is not MISSING else ".format("".join(self._line).strip()) ) elif token is Token.YIELD: if is_first_token(self._line): @@ -472,22 +543,27 @@ def _functions(self, token: Token) -> None: elif self._tokens[self._index - 1] in UNPACK_TRIGGERS: self._line.append("*") else: - self._line.append(".id") + self._line.append(".id()") else: # Token.MAIN self._line.append("entry ") def _multisemantic(self, token: Token) -> None: index = self._index if token is Token.TRY: - self._line.append("try" if is_first_token(self._line) else ".sm_random()") + self._line.append("try" if is_first_token(self._line) else ".random()") elif token is Token.TO: if self._tokens[index - 1] is Token.TABLE_OPEN: - self._line.append("null") + self._line.append("NULL") self._line.append("continue" if is_first_token(self._line) else ":") elif token is Token.FROM: - if isinstance(self._tokens[index + 1], str): - self._reg[Switch.IMPORT] = True - self._line.append("import_module('") + nxt = self._tokens[index + 1 : index + 4] + if ( + isinstance(nxt[0], str) + and nxt[1] is Token.ATTR + and isinstance(nxt[2], str) + ): + self._inline_counter = 4 + self._line.append("import_inline('") else: self._line.append("break") self._submit_line() @@ -507,7 +583,7 @@ def _multisemantic(self, token: Token) -> None: self._reg[Switch.CLASS_DEF] = True self._scope.enter("class") self._class_indent.append(self._indent) - self._line.append(f"@class_attributes\n{indent(self._indent)}class ") + self._line.append("class ") else: indented = self._indent > 0 self._line = [*self._line[:indented], "@", *self._line[indented:]] @@ -526,6 +602,8 @@ def _control_flow(self, token: Token) -> None: elif token is Token.ELSE: self._line.append(shift + "else ") else: # FOR + if self._tokens[self._index - 1] is Token.ELSE: + self._line.append("NULL") index = self._index if self._scope.current == "slice" and self._tokens[index + 1] is Token.ATTR: self._tokens[index] = self._tokens[index + 1] = Token.WHILE @@ -540,9 +618,11 @@ def _core(self, token: Token) -> None: Token.DEFAULT, Token.CATCH, }: - self._line.append("null") + self._line.append("NULL") if self._scope.current == "enum": - self._line.append('""", """') + if self._tokens[index - 2] in {token, Token.BRACE_OPEN}: + self._line.append("=NEXT") + self._line.append(",") return if self._reg[Switch.BUILTIN]: if self._line_tokens[-2] in {Token.EXIT, Token.SLEEP}: @@ -563,10 +643,10 @@ def _core(self, token: Token) -> None: start = self._indent > 0 assign_idx = self._line.index("=") stop = assign_idx - ( - self._line[assign_idx - 1] in {*"+-*%&|^", "**", "//"} + self._line[assign_idx - 1] in {*"+-*%&|^@", "**", "//"} ) variable = "".join(self._line[start:stop]) - self._line.append(f";verify_type({variable})") + self._line.append(f";{variable}=correct_type({variable})") self._submit_line() elif token is Token.ASSIGN: if self._line_tokens.count(token) > 1 and self._scope.current != "enum": @@ -575,9 +655,12 @@ def _core(self, token: Token) -> None: self._line.append("=") else: self._scope.exit() + elif token is Token.IMPORT: + self._reg[Switch.IMPORT] = True + self._line.append("import_to_scope('") elif token is Token.SEP: if self._tokens[index - 1] in NULLABLE_TOKENS: - self._line.append("null") + self._line.append("NULL") self._line.append(",") elif token is Token.ATTR: self._line.append(".") @@ -587,9 +670,9 @@ def _core(self, token: Token) -> None: self._line.append("self") elif token is Token.SLICE_OPEN: self._scope.enter("slice") - assign_index = find_next(self._tokens, Token.ASSIGN, after=index) - end_index = find_next(self._tokens, Token.END, after=index) - brace_index = find_next(self._tokens, Token.BRACE_OPEN, after=index) + assign_index = self._find_next(Token.ASSIGN) + end_index = self._find_next(Token.END) + brace_index = self._find_next(Token.BRACE_OPEN) self._slice_assign = ( 0 < assign_index @@ -597,7 +680,7 @@ def _core(self, token: Token) -> None: ) self._slice_object = self._tokens[index - 1] in SLICE_OBJECT_TRIGGERS if not self._slice_object: - self._line.append(f".sm_{'gs'[self._slice_assign]}et_item(") + self._line.append(f".__{'gs'[self._slice_assign]}etitem__(") self._line.append("mkslice(t(") elif token is Token.SLICE_CLOSE: if not self._slice_assign: @@ -612,7 +695,7 @@ def _core(self, token: Token) -> None: self._private = True return name = self._line[-1] - self._line.append(f'=Enum_(globals(), "{name}", """') + self._line.append(f"=Enum('{name}',") self._scope.enter("enum") def _builtins(self, token: Token) -> None: @@ -627,16 +710,26 @@ def _builtins(self, token: Token) -> None: and not self._line[-1].isspace() ): self._line.append(f"readline({self._line.pop()})") + else: + raise IndexError except IndexError: self._line.append("readline()") elif token is Token.THROW: if self._line_tokens[-2] in Group.operators: - self._line.append("null") + self._line.append("NULL") indented = self._indent > 0 self._line = [*self._line[:indented], "throw(", *self._line[indented:], ")"] elif token is Token.PRINT: - if self._line_tokens[-2] in Group.operators: - self._line.append("null") + if ( + self._scope.current == "class" + and is_first_token(self._line) + and self._tokens[self._index + 1] is not Token.END + ): + self._line.append("!") + return + with suppress(IndexError): + if self._line_tokens[-2] in Group.operators: + self._line.append("NULL") if "=" in self._line: hook = self._line.index("=") + 1 else: @@ -651,10 +744,13 @@ def _methods(self, token: Token) -> None: self._line.append(METHOD_MAPPING[token]) def _process_token(self, index: int, token: Tokenlike) -> None: - self._index = index self._line_tokens.append(token) + self._inline_counter -= 1 + if self._inline_counter == 0: + self._line.append("')") + # Integers if isinstance(token, int): self._line.append(f"Int({token})") diff --git a/src/samarium/utils.py b/src/samarium/utils.py index 1ba69d67..4549b242 100644 --- a/src/samarium/utils.py +++ b/src/samarium/utils.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import os import sys - -from contextlib import contextmanager, suppress +from collections.abc import Callable +from contextlib import contextmanager +from re import sub from string import digits, hexdigits, octdigits -from typing import Any, Callable, TypeVar, cast +from typing import Any, Iterator, TypeVar, cast -from .exceptions import NotDefinedError, SamariumTypeError, SamariumValueError +from .exceptions import SamariumTypeError, SamariumValueError from .tokenizer import Tokenlike -from .tokens import Token, OPEN_TOKENS, CLOSE_TOKENS +from .tokens import CLOSE_TOKENS, OPEN_TOKENS, Token -__version__ = "0.3.1" +__version__ = "0.4.0" T = TypeVar("T") @@ -22,8 +25,31 @@ } +KT = TypeVar("KT") +VT = TypeVar("VT") + + +class ClassProperty: + def __init__(self, func: Callable[..., Any]) -> None: + self.func = func + + def __get__(self, obj: Any, owner: Any | None = None) -> Any: + if obj is None: + obj = owner + return self.func(obj) + + +class Singleton: + _instances: dict[type[Singleton], Singleton] = {} + + def __new__(cls, *args: Any, **kwargs: Any): + if cls not in cls._instances: + cls._instances[cls] = super().__new__(cls, *args, **kwargs) + return cls._instances[cls] + + def match_brackets(tokens_: list[Tokenlike]) -> tuple[int, list[Token]]: - stack = [] + stack: list[Token] = [] token = Token.END tokens: list[Token] = [ cast(Token, t) for t in tokens_ if t in OPEN_TOKENS + CLOSE_TOKENS @@ -43,40 +69,30 @@ def match_brackets(tokens_: list[Tokenlike]) -> tuple[int, list[Token]]: return 0, [] -def readfile(path: str) -> str: - with open(path) as f: - return f.read() - - -def run_with_backup(main: Callable[..., T], backup: Callable[..., T], *args) -> T: - with suppress(NotDefinedError): - return main(*args) - return backup(*args) - - @contextmanager -def silence_stdout(): +def silence_stdout() -> Iterator[None]: stdout = sys.stdout sys.stdout = open(os.devnull, "w") yield sys.stdout = stdout -def sysexit(*args: Any): +def sysexit(*args: Any) -> None: if len(args) > 1: raise SamariumTypeError("=>! only takes one argument") - code = args[0].value if args else 0 + code = args[0].val if args else 0 if not isinstance(code, int): raise SamariumTypeError("=>! only accepts integers") - os._exit(code) + raise SystemExit(code) def parse_integer(string: str) -> int: + string = string.strip() orig = string neg = len(string) b = "d" if ":" in string: - b, string = string.split(":") + b, string = string.split(":", 1) if b not in "box": raise SamariumValueError(f"{b} is not a valid base") base = {"b": 2, "o": 8, "x": 16, "d": 10}[b] @@ -84,10 +100,28 @@ def parse_integer(string: str) -> int: digitset = {2: "01", 8: octdigits, 10: digits, 16: hexdigits}[base] neg -= len(string) neg %= 2 - if all(i in digitset for i in string.lower()): + if string and all(i in digitset for i in string.lower()): return int("-" * neg + string, base) - raise SamariumValueError(f'invalid string for Integer with base {base}: "{orig}"') + no_prefix = orig[2:] if orig[1] == ":" else orig + raise SamariumValueError( + f'invalid string for Integer with base {base}: "{no_prefix}"' + ) + + +def smformat(string: str, fields: str | list[Any] | dict[Any, Any]) -> str: + if isinstance(fields, str): + fields = [fields] + it = enumerate(fields) + if isinstance(fields, dict): + it = fields.items() + for k, v in it: + string = sub(rf"(? str: + return obj.__name__.removeprefix("sm_") -def get_callable_name(function: Callable) -> str: - return function.__name__.removeprefix("sm_") +def get_type_name(obj: Any) -> str: + return type(obj).__name__.removeprefix("sm_")