├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.md ├── COPYING ├── Makefile ├── README.md ├── STYLE_GUIDE.universal.md ├── STYLE_GUIDE.zig.md ├── WORDS.md ├── build.zig ├── init.gale ├── lib └── gale │ ├── gale.zig │ ├── helpers.zig │ ├── internal_error.zig │ ├── nucleus_words.zig │ ├── object.zig │ ├── parsed_word.zig │ ├── rc.zig │ ├── runtime.zig │ ├── shape.zig │ ├── stack.zig │ ├── test_gale.zig │ ├── test_helpers.zig │ ├── type_system_tests.zig │ ├── types.zig │ ├── well_known_entities.zig │ ├── word.zig │ ├── word_list.zig │ ├── word_map.zig │ └── word_signature.zig ├── sketches ├── bounded_parameters.gale ├── branching_overloading.gale ├── sameness_checker.gale ├── simple_shapes.gale └── union_types_instead_of_enums.gale ├── src └── gale │ └── main.zig └── tests └── test_protolang.zig /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: klardotsh 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | .ignore 3 | .pijul 4 | zig-cache 5 | src/zig-cache 6 | zig-out 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Gale 2 | 3 | Currently, the advice is mostly "don't", but this document exists to those who 4 | disregard such advice (thanks for taking the leap of faith!), and to start 5 | laying the groundwork for some future where, maybe, things are in a better 6 | state to contribute to. It also helps remind *myself* of some things... 7 | 8 | ## On Theming 9 | 10 | Gale is a term most frequently associated with weather at sea, but sea stuff has 11 | already been beaten to death by another (quite popular) open source ecosystem, 12 | so note that Gale tools are named after meteorological phenomena: think wind, 13 | clouds, rain, snow, etc. and less parts of boats or sea creatures or waves or 14 | whatever. 15 | 16 | ## Style Guides 17 | 18 | See [the universal style guide for Gale](STYLE_GUIDE.universal.md) and [the Zig 19 | style guide for Gale](STYLE_GUIDE.zig.md) for now. Neither are strictly 20 | enforced yet, and especially the Zig one needs some updates to reflect the 21 | reality that has organically grown in the codebase, but it's a start. 22 | 23 | ## Version Control And Maintenance Hygiene 24 | 25 | > To a great degree, this guidance is inspired by [Zulip's Commit Discipline 26 | > guide](https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html), 27 | > which out of everywhere I've worked and all the codebases I've read in 28 | > professional or hobbyist contexts, had probably the most readable and 29 | > functional Git log. Read that guide to understand the context and 30 | > inspirations for this one, if you're so interested. 31 | 32 | - Commits must follow the message wording guide described in the subsection 33 | below. This will be enforced with tooling 34 | ([`gitlint`](https://jorisroovers.com/gitlint/) or perhaps a bespoke 35 | analogue) wherever feasible. 36 | - Commits should be as small as feasibly makes an individual reviewable unit, 37 | and no smaller. A brand new component can often come through in one commit, 38 | but when refactoring existing code to set things up for cleanly adding a 39 | feature, the refactor should almost always live separately from that new 40 | feature. 41 | - Tests must all pass on each commit, and thus updated and/or net-new tests 42 | should always be included in the same commit as the work that neccessitated 43 | them. It is never acceptable to retain "flakey" tests (those that only work 44 | sometimes, and perhaps break depending on the time of day or the availability 45 | of a network connection): if discovered during patchset review, they must be 46 | fixed before the work can be merged. If discovered on a trunk or integration 47 | branch, they should be fixed as soon as possible (ideally by the original 48 | author of the test, if possible). 49 | - As a general rule, merge commits are unacceptable anywhere other than 50 | integration branches, and should only be made by project maintainers (for 51 | now, that means @klardotsh). Patchsets including merge commits (*especially* 52 | merge commits pulling from the patchset's target branch) will be rejected: 53 | the net-new commits should always be cleanly rebased on top of the target 54 | branch. 55 | - GPG and/or SSH signatures for commits are strongly encouraged (see, for 56 | example [this article about signing with SSH 57 | keys](https://blog.dbrgn.ch/2021/11/16/git-ssh-signatures/)). 58 | - Commits (except those authored by `@klardotsh` and signed with his keys) must 59 | be `Signed-off-by` for acceptance to the tree, indicating the author of the 60 | commit has read, acknowledged, and agrees to the [Developer Certificate of 61 | Origin](https://developercertificate.org/). For a bit of a layman's 62 | explanation of the DCO and how it interacts with `git commit -s` and 63 | `Signed-off-by`, see [Drew DeVault's blog post on the 64 | subject](https://drewdevault.com/2021/04/12/DCO.html). 65 | 66 | ### Commit Messages 67 | 68 | Commit messages should take the format of: 69 | 70 | ``` 71 | section[/subsection]: Provide succinct active-voice description of changes. 72 | 73 | Details go in the body of the commit message and can wax poetic about the hows 74 | and whys as the author sees fit. The title, however, should be no more than 72 75 | characters long unless it's impossible to condense further without losing 76 | crucial information (in which case, the *hard* limit is 100 characters). 77 | 78 | You can use tags like these as necessary: 79 | 80 | Refs: https://example.com/link/to/bug/tracker/1234 81 | Co-authored-by: My Shadow 82 | ``` 83 | 84 | Examples of sections might include `docs:` or `devxp:` or `perf:`, or some 85 | section of the codebase, like `word_signature:`. Often, subsections may be 86 | useful, for example: `std/fs: Add docstrings throughout.`. 87 | 88 | 89 | ### Non-Text Files 90 | 91 | Non-text files ("binaries") must **never** be checked into Git directly, as 92 | they bloat the clone size of the repo _forever_, not just for the time that 93 | the version of the file is reachable in the directory tree (since Git stores 94 | objects permanently to allow local checkouts of prior commits, every revision 95 | to, say, an image, must be cloned). Use [Git LFS](https://git-lfs.com/) for non- 96 | text files. Prefer LFS over scripts that download binaries to the developer's 97 | workstation "at runtime", unless licensing or other restrictions mandate that 98 | the files can't be redistributed via LFS. 99 | 100 | A great way to avoid checking non-text files in *anywhere* is to only preserve 101 | the plain-text sources that are used to generate said binaries. For source 102 | code, this is already an unwritten expectation within most projects ("hey, maybe 103 | don't check in that unsigned binary you built on a box in your basement?"), but 104 | where this becomes crucial is in imagery: I find this is most often the cause of 105 | bloated Git repos. Anything that can be stored SVG-based vector imagery *should 106 | be*, but anything inherently rasterized (say, screenshots) will have to live 107 | in LFS. 108 | 109 | > It should be noted that the canonical repo for Gale 110 | > is hosted on Sourcehut, which [as of 2023 still does not support Git 111 | > LFS](https://lists.sr.ht/~sircmpwn/sr.ht-discuss/%3CCAG+K25NORsCEpUQ%3DMP_iD5yEwn1v259g2jqr4ykjdX6RCZxoXw%40mail.gmail.com%3E) 112 | > despite years of indicating intent. Currently, there's also no binary files 113 | > in the tree. I'll deal with this problem whenever it becomes relevant: 114 | > potentially by finding a dedicated LFS host and configuring the repo to use 115 | > it, or potentially by finding a new Git host (again: there was already one 116 | > quiet migration from GitHub to Sourcehut). 117 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | 3 | .PHONY: lint 4 | lint: 5 | ziglint -skip todo 6 | 7 | .PHONY: test 8 | test: 9 | zig build test 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gale: small-but-mighty, strongly-typed, concatenative development at storm-force speed 2 | 3 | Gale is a [concatenative programming 4 | language](https://en.wikipedia.org/wiki/Concatenative_programming_language) 5 | which: 6 | 7 | - Has a strong, 8 | [dynamic](https://en.wikipedia.org/wiki/Type_system#Dynamic_type_checking_and_runtime_type_information), 9 | and generally 10 | [structural](https://en.wikipedia.org/wiki/Structural_type_system) type 11 | system that offers a moderate degree of type inference. 12 | 13 | - Is designed interactively-first, with REPL and [Language 14 | Server](https://en.wikipedia.org/wiki/Language_Server_Protocol) experiences 15 | as first-class citizens, and as such encourages rapid prototyping and 16 | experimentation. 17 | 18 | - Can be embedded within Zig and C applications (and those written in languages 19 | supporting C FFI). 20 | 21 | - Sports a very small implementation: currently well under 5k lines of Zig 22 | (subtracting comments and blank lines) gets a runtime off the ground. 23 | 24 | 25 | 26 | - Is extremely permissively licensed (`0BSD`, public domain equivalent) 27 | 28 | ## What's it look like? 29 | 30 | See the `sketches/` tree for now, which is in a constant state of flux and not 31 | necessarily always kept up to date with what I'm aiming for, but I try. 32 | 33 | ## Navigating this repo 34 | 35 | - `lib/gale` is a pure-Zig implementation of the Gale nucleus as a library 36 | - `src/gale` builds on these to provide a thin CLI that works with the usual 37 | stdin/stdout/stderr 38 | - `tests/` includes various end-to-end tests of language functionality that 39 | didn't cleanly fit as unit tests in the above categories 40 | 41 | ## Supporting Gale's Development 42 | 43 | Currently, Gale is just a nights-and-weekends side project whenever my rather 44 | busy life allows, and as with any hobby, I don't expect payment for it. For now, 45 | simply riffing ideas with me and experimenting with Gale as it grows is payment 46 | enough. If you really insist you want to financially support Gale in this 47 | extremely early phase, there's a LiberaPay link in the `.github/` tree. 48 | 49 | ## Legal Yadda-Yadda 50 | 51 | Gale's canonical implementation and standard library is released under the 52 | [Zero-Clause BSD License](https://tldrlegal.com/license/bsd-0-clause-license), 53 | distributed alongside this source in a file called `COPYING`, and also found 54 | at the top of each source file. 55 | 56 | Further, while not a legally binding mandate, I ask that you have fun with it, 57 | build cool stuff with it, don't exploit your fellow humans or the world at 58 | large with it, and generally don't be an ass within or outside of the project 59 | or anything written with it. And if you want to give attribution, it's 60 | of course also appreciated. 61 | -------------------------------------------------------------------------------- /STYLE_GUIDE.universal.md: -------------------------------------------------------------------------------- 1 | ## Legal Comments 2 | 3 | "Why, oh why," you might ask, "does every single file in this source tree start 4 | with a header comment reminding me that it is released under the Zero-Clause 5 | BSD License, distributed blah blah blah?" 6 | 7 | Great question. 8 | 9 | Mostly, it serves to remind anyone reading the file that they can do, 10 | effectively, whatever they want with it. This is not a given in western legal 11 | systems (and in particular, in the US), and since the spirit of Gale is to 12 | freely copy files around into monorepos or forks, and generally to have 13 | complete flexibility in how files are structured (or unstructured...) on disk, 14 | it helps ensure a file that floats away from this repo still reminds the user 15 | that it is not encumbered. 16 | 17 | As an extension of the above, it serves as a bit of a repetitive broadcast of 18 | @klardotsh's general disdain for enforcing (or trying to enforce) software 19 | licenses (believe it or not, I used to be a GPL die-hard), for restrictive and 20 | absurdly-long-lived copyright law (my life plus 70 years by US law), and for 21 | "intellectual property" as a concept. Separating the code from any pay related 22 | to the time spent writing it is a model I want to explore and see explored more 23 | in software. 24 | -------------------------------------------------------------------------------- /STYLE_GUIDE.zig.md: -------------------------------------------------------------------------------- 1 | # Gale style guide for Zig code 2 | 3 | In general, `zig fmt` is the final arbiter of truth for things like line 4 | length, indentation, etc. This document describes things we have control over 5 | that `zig fmt` won't overwrite. Note that @klardotsh is the sole arbiter of 6 | aesthetics (and indeed all things in the language); words like "Pretty", 7 | "Ugly", etc. are judged by his eyes. They are capitalized in this document to 8 | emphasize their fuzzy-subjective-ness. 9 | 10 | ## Comment the hell out of everything 11 | 12 | This one doesn't even require an example, it's just the First Commandment of 13 | this codebase. Even if you screw up the entire rest of this style guide, follow 14 | this point. 15 | 16 | You see [Jones Forth](https://github.com/nornagon/jonesforth)? You see how it 17 | reads like the Lord of the freakin' Rings? Do that. My personal philosophy on 18 | comments is to be far more liberal with them than is generally taught in 19 | schools or in (startup-land, at least) industry, and holds much closer to [the 20 | opinions of antirez](http://antirez.com/news/124), of Redis fame. We're writing 21 | system software here, which is an inherently non-trivial domain. Help people 22 | who might be coming from higher-level languages or less programming experience 23 | understand what we're doing, and empower them to contribute (or build their own 24 | low-level stuff!). We all started somewhere. 25 | 26 | ## All constants with semantic meaning should be named 27 | 28 | Giving constants names makes their intent obvious and survives the renames of 29 | functions. It also makes refactoring to make constants modifiable at build time 30 | (with `build.zig` arguments) easier. 31 | 32 | ```zig 33 | // Bad: why 8? and why can't I change this anywhere??? 34 | const foo = [_]u8{0} ** 8; 35 | 36 | // Fixed: magic constant now has a name, and overrides could theoretically be 37 | // plumbed without digging into the code around foo itself. 38 | const NUMBER_OF_FOOS = 8; // in reality there's 6, plus one. IYKYK, RIP. 39 | // ... 40 | const foo = [_]u8{0} ** NUMBER_OF_FOOS; 41 | ``` 42 | 43 | ## Don't make visually clutterful blocks 44 | 45 | Except when it would cause line length to exceed 80ish characters or would just 46 | otherwise look Ugly (such as by doing too many things in too little space at 47 | the expense of reading comprehension), "one-liner" statements should be just 48 | that: one line. This does not supercede "Give things vertical breathing room", 49 | below: single-line statements should have empty lines immediately above and 50 | below them, with no exceptions. 51 | 52 | Good: 53 | 54 | ```zig 55 | // Good: trivial if statement that does exactly one thing and fits well under 56 | // 80 characters. 57 | if (!any_alive_remaining) alloc.free(compound); 58 | 59 | // Bad: there's just too much going on in one line here: a function call, a 60 | // capture group, another function call, and an assignment. Further, in the 61 | // context this was pulled from, the line reaches 97 characters counting 62 | // indentation. 63 | while (symbol_iter.next()) |entry| _ = entry.decrement_and_prune(.FreeInner, self.alloc); 64 | 65 | // Fixed: 66 | while (symbol_iter.next()) |entry| { 67 | _ = entry.decrement_and_prune(.FreeInner, self.alloc); 68 | } 69 | ``` 70 | 71 | ## Give things vertical breathing room 72 | 73 | Unlike in IRC, extensive use of the `Enter` key is welcome in our Zig code 74 | where it breaks a function up into logical chunks that don't otherwise make 75 | sense to split into their own functions. Further, branching and looping 76 | statements (`if`, `switch`, `for`, `while`) should **always** be separated from 77 | other code with blank lines on both sides, with no exceptions. Note the 78 | interaction of this rule with "Don't make visually clutterful blocks" above. 79 | 80 | ```zig 81 | // Bad: This reads like a text from someone who just learned how texting works 82 | // and doesn't bother to use punctuation, newlines, or multiple messages in 83 | // sequence. 84 | var runtime = try Runtime.init(testAllocator); 85 | defer runtime.deinit(); 86 | const heap_for_word = try runtime.word_from_primitive_impl(&push_one); 87 | runtime.stack = try runtime.stack.do_push(Object{ .Word = heap_for_word }); 88 | runtime.stack = try runtime.stack.do_push(Object{ .Boolean = true }); 89 | try CONDJMP(&runtime); 90 | const should_be_1 = try runtime.stack.do_pop(); 91 | try expectEqual(@as(usize, 1), should_be_1.UnsignedInt); 92 | runtime.stack = try runtime.stack.do_push(Object{ .Word = heap_for_word }); 93 | runtime.stack = try runtime.stack.do_push(Object{ .Boolean = false }); 94 | try CONDJMP(&runtime); 95 | try expectError(StackManipulationError.Underflow, runtime.stack.do_pop()); 96 | 97 | // Fixed: splits that dense block up into logical sections: 98 | // - High level initialization (since this excerpt is from a test, runtime 99 | // isn't coming as a function argument as it normally might) 100 | // - Local initialization 101 | // - Logical unit: testing truthy case 102 | // - Logical unit: testing falsey case 103 | var runtime = try Runtime.init(testAllocator); 104 | defer runtime.deinit(); 105 | 106 | const heap_for_word = try runtime.word_from_primitive_impl(&push_one); 107 | 108 | runtime.stack = try runtime.stack.do_push(Object{ .Word = heap_for_word }); 109 | runtime.stack = try runtime.stack.do_push(Object{ .Boolean = true }); 110 | try CONDJMP(&runtime); 111 | const should_be_1 = try runtime.stack.do_pop(); 112 | try expectEqual(@as(usize, 1), should_be_1.UnsignedInt); 113 | 114 | runtime.stack = try runtime.stack.do_push(Object{ .Word = heap_for_word }); 115 | runtime.stack = try runtime.stack.do_push(Object{ .Boolean = false }); 116 | try CONDJMP(&runtime); 117 | try expectError(StackManipulationError.Underflow, runtime.stack.do_pop()); 118 | ``` 119 | -------------------------------------------------------------------------------- /WORDS.md: -------------------------------------------------------------------------------- 1 | # Gale Core Words Reference 2 | 3 | This document describes all words in Gale's Core - in the reference 4 | implementation, this refers to any words provided by the Nucleus or Prelude. 5 | 6 | All word signatures in this document use the fully qualified right-pointing 7 | form, which is to say, the stack will be taken from the arrow leftwards, and 8 | given from the arrow rightwards. Thus, `@2 @1 -> @1 @2` will take a generic 9 | (kind 1) from the top of the stack, then another generic (kind 2) from the 10 | now-top of the stack, do... whatever with them, and eventually place objects 11 | of kind 1 and kind 2, respectively and orderly, onto the top of the stack, 12 | performing an effective swap. Technically word signatures alone aren't 13 | enough to know that @1 and @1 are the same objects in memory: they'll simply 14 | be the same Shape. You'll need to read the docs and/or implementation to 15 | make "object in memory" assertions. 16 | 17 | ## Primitives 18 | 19 | The Nucleus understands a few data types out of the box, as follows. 20 | Particularly in the case of numbers, trailing slashes and suffixes can be used 21 | to disambiguate datatypes (eg. between signed and unsigned integers within 22 | range). 23 | 24 | - `42`, `42/u`: unsigned integers, aligned to the native bit-size of the 25 | system. 26 | 27 | - `-1`, `42/i`: signed integers, aligned to the native bit-size of the system. 28 | 29 | - ` 30 | 31 | - `"strings"`: a sequence of valid UTF-8 codepoints. Double quotes within the 32 | string can be escaped with `\`, and thus a raw `\` character must also be 33 | escaped as `\\`. 34 | 35 | ## Trusting Words 36 | 37 | These words sit at the absolute lowest level of the Gale Nucleus, implementing 38 | memory management in "unsafe" ways. Unsafe is a rather strong word with rather 39 | strong connotations: well-tested code that accesses raw memory is not 40 | inherently "unsafe", it simply must be heavily tested, and trusted. Thus, they 41 | are called "Trusting Words". Their risky nature is emphasized stylistically by 42 | way of their being `!UPPERCASED`, and prefixed with an exclamation point. They 43 | look a bit more FORTH-y than most of Gale's vocabulary. 44 | 45 | ## Generic Stack Manipulation 46 | 47 | All of these live in the Global mode, as they apply regardless of execution 48 | state. 49 | 50 | | Word | Signature | Notes | 51 | |-----------|---------------------------------------|-------| 52 | | id | `@1 -> @1` | | 53 | | dup | `@1 -> @1 @1` | | 54 | | dupn2 | `@2 @1 -> @2 @1 @2` | | 55 | | dupn3 | `@3 @2 @1 -> @3 @2 @1 @3` | | 56 | | dupn4 | `@4 @3 @2 @1 -> @4 @3 @2 @1 @4` | | 57 | | 2dup | `@2 @1 -> @2 @2 @1 @1` | | 58 | | 2dupshuf | `@2 @1 -> @2 @1 @2 @1` | | 59 | | swap | `@2 @1 -> @1 @2` | | 60 | | pairswap | `@4 @3 @2 @1 -> @2 @1 @4 @3` | | 61 | | yoinkn3 | `@3 @2 @1 -> @2 @1 @3` | [^1] | 62 | | yoinkn4 | `@4 @3 @2 @1 -> @3 @2 @1 @4` | [^1] | 63 | | twist | `@3 @2 @1 -> @1 @2 @3` 64 | | cartwheel | `@4 @3 @2 @1 -> @1 @2 @3 @4` | | 65 | | drop | `@1 -> nothing` | | 66 | | 2drop | `@2 @1 -> nothing` | | 67 | | 3drop | `@3 @2 @1 -> nothing` | | 68 | | 4drop | `@4 @3 @2 @1 -> nothing` | | 69 | | zapn2 | `@2 @1 -> @1 /* ~= swap drop */` | [^2] | 70 | | zapn3 | `@3 @2 @1 -> @2 @1 /* ~= rot drop */` | [^2] | 71 | | zapn4 | `@4 @3 @2 @1 -> @3 @2 @1` | [^2] | 72 | 73 | [^1]: There is no `yoink` or `yoinkn2`. `yoink` would just be `id`, and 74 | `yoinkn2` is `swap`. 75 | 76 | [^2]: There is no `zap`, as it is functionally equivalent to `drop` While 77 | `zapn2` and `zapn3` have logical equivalents as documented in the table, 78 | their implementations are able to be optimized and thus stand alone. 79 | 80 | ## Word, Mode, and Shape Manipulation 81 | 82 | These are implemented twice each: 83 | 84 | * Once in Build mode, for _statically-initiated_ definitions 85 | * Again in Run mode, for manipulating these within methods (allowing runtime 86 | metaprogramming) 87 | 88 | 89 | This distinction is important: the Run mode varieties have to trigger special, 90 | performance-impacting behaviors in the Runtime, notably invoking the 91 | Pollutifier and Surveyor systems (used to retain static analysis of 92 | metaprogrammed Gale code and to find any now-invalidated code caused by the 93 | redefinition). Suffice to say, Gale encourages (and is designed around) knowing 94 | as much as you can about your Words, Modes, and Shapes at Build time, but 95 | allows for useful runtime metaprogramming - for a price. 96 | 97 | | Word | Signature | Notes | 98 | |------------------------|---------------------------------------|-------| 99 | | Shape! | `nothing -> Shape` | | 100 | | register-globally | `Shape Identifier -> nothing` | | 101 | | register-modally | `Shape Identifier Mode -> nothing` | | 102 | | enum-member | `Identifier Shape -> Shape` | | 103 | | enum-member-containing | `Shape Identifier Shape -> Shape` | | 104 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | 16 | const pkgs = struct { 17 | const gale = std.build.Pkg{ 18 | .name = "gale", 19 | .source = .{ .path = "lib/gale/gale.zig" }, 20 | .dependencies = &[_]std.build.Pkg{}, 21 | }; 22 | }; 23 | 24 | pub fn build(b: *std.build.Builder) void { 25 | // Standard target options allows the person running `zig build` to choose 26 | // what target to build for. Here we do not override the defaults, which 27 | // means any target is allowed, and the default is native. Other options 28 | // for restricting supported target set are available. 29 | const target = b.standardTargetOptions(.{}); 30 | 31 | // Standard release options allow the person running `zig build` to select 32 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 33 | const mode = b.standardReleaseOptions(); 34 | 35 | const lib = b.addStaticLibrary("gale", "lib/gale/gale.zig"); 36 | lib.setBuildMode(mode); 37 | lib.install(); 38 | 39 | const exe = b.addExecutable("gale", "src/gale/main.zig"); 40 | exe.setTarget(target); 41 | exe.setBuildMode(mode); 42 | exe.addPackage(pkgs.gale); 43 | exe.install(); 44 | 45 | const run_cmd = exe.run(); 46 | run_cmd.step.dependOn(b.getInstallStep()); 47 | if (b.args) |args| { 48 | run_cmd.addArgs(args); 49 | } 50 | 51 | const run_step = b.step("run", "Run the app"); 52 | run_step.dependOn(&run_cmd.step); 53 | 54 | // gale library and core tests 55 | const lib_tests = b.addTest("lib/gale/test_gale.zig"); 56 | lib_tests.setTarget(target); 57 | lib_tests.setBuildMode(mode); 58 | 59 | // gale CLI tests 60 | const exe_tests = b.addTest("src/gale/main.zig"); 61 | exe_tests.setTarget(target); 62 | exe_tests.setBuildMode(mode); 63 | 64 | // End-to-end tests of the protolang 65 | const protolang_tests = b.addTest("tests/test_protolang.zig"); 66 | protolang_tests.setTarget(target); 67 | protolang_tests.setBuildMode(mode); 68 | protolang_tests.addPackage(pkgs.gale); 69 | 70 | const test_step = b.step("test", "Run unit tests"); 71 | test_step.dependOn(&lib_tests.step); 72 | test_step.dependOn(&exe_tests.step); 73 | test_step.dependOn(&protolang_tests.step); 74 | } 75 | -------------------------------------------------------------------------------- /init.gale: -------------------------------------------------------------------------------- 1 | { 2 | Copyright (C) 2023 Josh Klar aka "klardotsh" 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | } 15 | 16 | // Welcome to init.gale, the bring-up file for the canonical Gale 17 | // implementation, which is designed around using the host language (in our 18 | // case, Zig) as little as possible, and self-hosting as much as possible in 19 | // Gale itself. As such, this is almost certainly not the most performant 20 | // possible implementation of Gale: whether I circle back to address this is 21 | // left as a debate for my future self. That said, the desire to 22 | // nearly-entirely self-host Gale led to many of its design decisions, 23 | // especially the ability to be both a relatively high-level language and a 24 | // (perhaps less performant than C/Zig/Rust) lower-ish level language at the 25 | // same time. It is also where the concept of "Trusting Words", which we'll 26 | // learn more about in a bit, came from. From here on, we'll take "Gale", "the 27 | // Gale Nucleus", etc. to refer to this canonical implementation. 28 | 29 | // Out of the box, the Gale Nucleus provides us with almost nothing: this file 30 | // is read off the disk as a buffered stream and run through the exceptionally 31 | // primitive word parser which understands: 32 | // 33 | // - // through end of line as a comment 34 | // - signed and unsigned integers (42, -42, 42/u, 42/i, etc.) 35 | // * TODO: suffixes impl 36 | // - symbols, references, and bare-word references ('Foo, &Bar, swap) 37 | // 38 | // The default collection of words is also extremely sparse: we have some basic 39 | // Trusting Word implementations of each of the Generic Stack Manipulation 40 | // words described in `WORDS.md`, of each of the Memory Manipulation words from 41 | // the same document, and then some words specific to this implementation of 42 | // Gale to provide hooks to the runtime, and ways to override default 43 | // behaviors. However, *not* included are pretty much any of the 44 | // actually-interesting details of the language: ergonomic ways to define 45 | // words, any concept of shapes or signatures or dynamic word dispatch, 46 | // docstrings, etc. We'll build all of that right here. We'll be building this 47 | // in "top-down" fashion: given a "userspace" concept as advertized in the 48 | // documentation or example files, we'll build it, and any of its dependent 49 | // concepts and structures that have yet to be built. Thus, the ordering of 50 | // this file will be a *bit* more jumbly than, say, JonesFORTH if you've read 51 | // that (which in general takes a more bottom-up approach, which is admittedly 52 | // more readable even to me...). 53 | 54 | // Before we get into defining our first bits of functionality, this is a great 55 | // time to go over a bit of Style and Convention: both in this file, and for 56 | // Gale as a language ecosystem: 57 | // 58 | // - Words generally are lower-kebab-cased (eg. `say-hello-world`) 59 | // - Exception 1: Trusting Words are always screaming-snake-cased with a 60 | // leading exclamation point (eg. `!ALLOCATE_SOMETHING`) 61 | // - Exception 2: Nucleus Words are always screaming-snake-cased with a 62 | // leading at-sign (eg. `@NUM_OBJECTS_ALLOCATED`) 63 | // - Shapes are almost always Pascal-cased (eg. `FooBarAble`) 64 | // - As a general guideline, the acting working stack for a word should quite 65 | // rarely be deeper than four objects 66 | 67 | // First, let's get some prettier syntax in here by creating our first word - 68 | // :!, to create a Trusting Word. Since we have no type system yet (nor Shapes 69 | // to populate it with), this is not yet the spec-compliant version of this 70 | // word, just a low-level helper for bootstrapping. 71 | // 72 | // Usage: :! !ALLOCATE_BLOCK @SIZED_OPAQUE ; 73 | // ^ This is not actually the implementation of !ALLOCATE_BLOCK we'll 74 | // land on, read along for that. 75 | 76 | // I really, really, do not want to implement Immediate Words in Gale, not even 77 | // in this weird bootstrappy form of the language that will never see userspace 78 | // light of day. If you write a Gale implementation, you're welcome to use 79 | // Immediate Words to solve problems like this. I will instead abuse a "private 80 | // space" (implementation and documentation found over in Zig-land), and start 81 | // prescribing meaning to the empty data therein. Our first contestant: a u8 82 | // byte dedicated to an enumeration of interpreter states, enabling us to move 83 | // from "execution mode" into "symbol mode" (where bare words immediately 84 | // become symbols, even if they exist in the Word or Shape dictionaries) or 85 | // into "ref mode" (bare words become refs to whatever the identifier points 86 | // to, if they are defined. At this low level, unresolveable refs simply panic 87 | // the interpreter). 88 | // 89 | // Since enums in the Gale sense require a type system too, we'll just define 90 | // words to put names to these enum members. See? We're not so unlike a FORTH 91 | // after all :) Since ! and @ are taken, my non-kernel "private" words will be 92 | // %-prefixed. 93 | 94 | // @LIT ( @1 -> Word ) 95 | // Wraps any value type in an anonymous word which will return that value when 96 | // called. Generally useful when defining words which need to refer to numbers, 97 | // strings, symbols, etc. at runtime. 98 | 99 | // Keep these in sync with runtime.zig 100 | // :! %INTERP_MODE_EXEC 0/u ; 101 | 0/u @LIT '%INTERP_MODE_EXEC @DEFINE-WORD-VA1 102 | // :! %INTERP_MODE_SYMBOL 1/u ; 103 | 1/u @LIT '%INTERP_MODE_SYMBOL @DEFINE-WORD-VA1 104 | // :! %INTERP_MODE_REF 2/u ; 105 | 2/u @LIT '%INTERP_MODE_REF @DEFINE-WORD-VA1 106 | 107 | // For convenience, let's make some toggle words for these states: 108 | 109 | // :! %PS_INTERP_MODE 0/u ; 110 | 0/u @LIT '%PS_INTERP_MODE @DEFINE-WORD-VA1 111 | // :! %>_ %PS_INTERP_MODE @PRIV_SPACE_SET_BYTE ; 112 | &@PRIV_SPACE_SET_BYTE %PS_INTERP_MODE '%>_ @DEFINE-WORD-VA1 113 | // :! %>EXEC %INTERP_MODE_EXEC %>_ ; 114 | &%>_ %INTERP_MODE_EXEC '%>EXEC @DEFINE-WORD-VA3 115 | // :! %>SYMBOL %INTERP_MODE_SYMBOL %>_ ; 116 | &%>_ %INTERP_MODE_SYMBOL '%>SYMBOL @DEFINE-WORD-VA3 117 | // :! %>REF %INTERP_MODE_REF %>_ ; 118 | &%>_ %INTERP_MODE_REF '%>REF @DEFINE-WORD-VA3 119 | 120 | // @PRIV_SPACE_SET_BYTE ( UInt8 UInt8 -> nothing ) 121 | // | | 122 | // | +-> address to set 123 | // +-------> value to set 124 | 125 | // As discussed over in the Nucleus source, interpreter modes take effect for 126 | // exactly one word by default. However, we can override this behavior using 127 | // @BEFORE_WORD ( Word -> nothing ), where the word referenced is of the 128 | // signature ( Symbol <- nothing ). The symbol represents the word about to be 129 | // processed exactly as it was passed to the interpreter. It must be left on 130 | // the stack for the word to be handled (if removed, the word is silently 131 | // ignored. This can almost certainly be used for all sorts of insane stuff I 132 | // haven't thought of yet, have fun.) 133 | 134 | // @CONDJMP2 ( Word Word Boolean -> nothing ) 135 | // 136 | // Immediately executes the near word if the boolean is truthy, and the far 137 | // word otherwise. Effectively all userspace conditionals can be built with 138 | // this primitive and its else-less counterpart, 139 | // @CONDJMP ( Word Boolean -> nothing ) 140 | 141 | // :! %REF_WORDS_UNTIL_SEMICOLON '; @EQ! %>REF %>EXEC @CONDJMP2 ; 142 | &@CONDJMP2 143 | &%>EXEC &%>REF 144 | &@ZAPN2 &@EQ '; 145 | '%REF_WORDS_UNTIL_SEMICOLON @DEFINE-WORD-VA 146 | 147 | // @EQ ( @2 @1 <- Boolean ) 148 | -------------------------------------------------------------------------------- /lib/gale/gale.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | pub const InternalError = @import("./internal_error.zig"); 15 | pub const Runtime = @import("./runtime.zig").Runtime; 16 | pub const Types = @import("./types.zig"); 17 | -------------------------------------------------------------------------------- /lib/gale/helpers.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const expect = std.testing.expect; 16 | 17 | // Just silly stuff that's nice to access by name 18 | pub const CHAR_AMPER = '&'; 19 | pub const CHAR_COLON = ':'; 20 | pub const CHAR_COMMA = ','; 21 | pub const CHAR_DOT = '.'; 22 | pub const CHAR_HASH = '#'; 23 | pub const CHAR_NEWLINE = '\n'; 24 | pub const CHAR_QUOTE_SGL = '\''; 25 | pub const CHAR_QUOTE_DBL = '"'; 26 | pub const CHAR_SPACE = ' '; 27 | pub const CHAR_TAB = '\t'; 28 | pub const EMPTY_STRING = ""; 29 | 30 | pub fn bool_from_human_str(val: []const u8) bool { 31 | if (std.mem.eql(u8, val, "")) { 32 | return false; 33 | } 34 | 35 | inline for (.{ "1", "true", "TRUE", "yes", "YES" }) |pattern| { 36 | if (std.mem.eql(u8, val, pattern)) { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | 44 | test "bool_from_human_str" { 45 | try expect(bool_from_human_str("1")); 46 | try expect(bool_from_human_str("true")); 47 | try expect(bool_from_human_str("TRUE")); 48 | try expect(bool_from_human_str("yes")); 49 | try expect(bool_from_human_str("YES")); 50 | try expect(bool_from_human_str("") == false); 51 | try expect(bool_from_human_str("0") == false); 52 | try expect(bool_from_human_str("no") == false); 53 | try expect(bool_from_human_str("2") == false); 54 | try expect(bool_from_human_str("narp") == false); 55 | } 56 | 57 | /// Pluck common boolean representations from an environment variable `name` as 58 | /// an actual boolean. 1, true, TRUE, yes, and YES are accepted truthy values, 59 | /// anything else is false. 60 | pub fn getenv_boolean(name: []const u8) bool { 61 | return bool_from_human_str(std.os.getenv(name) orelse ""); 62 | } 63 | 64 | test { 65 | std.testing.refAllDecls(@This()); 66 | } 67 | -------------------------------------------------------------------------------- /lib/gale/internal_error.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | 16 | pub const InternalError = error{ 17 | AttemptedDestructionOfPopulousRc, 18 | AttemptedResurrectionOfExhaustedRc, // me too, buddy 19 | BoundedShapeWithoutBoundsCheckingWord, 20 | EmptyWord, 21 | InvalidWordName, 22 | TypeError, 23 | Unimplemented, 24 | ValueError, // TODO: rename??? 25 | }; 26 | 27 | test { 28 | std.testing.refAllDecls(@This()); 29 | } 30 | -------------------------------------------------------------------------------- /lib/gale/nucleus_words.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const Allocator = std.mem.Allocator; 16 | const testAllocator: Allocator = std.testing.allocator; 17 | const expect = std.testing.expect; 18 | const expectEqual = std.testing.expectEqual; 19 | const expectError = std.testing.expectError; 20 | 21 | const _stack = @import("./stack.zig"); 22 | const test_helpers = @import("./test_helpers.zig"); 23 | 24 | const InternalError = @import("./internal_error.zig").InternalError; 25 | const Runtime = @import("./runtime.zig").Runtime; 26 | const StackManipulationError = _stack.StackManipulationError; 27 | const Word = @import("./word.zig").Word; 28 | const WordSignature = @import("./word_signature.zig").WordSignature; 29 | 30 | // As a general rule, only write tests for methods in this file that actually 31 | // do something noteworthy of their own. Some of these words call directly into 32 | // Stack.whatever() without meaningful (if any) handling, duplicating those 33 | // tests would be pointless. 34 | 35 | /// @EQ ( @2 @1 <- Boolean ) 36 | /// 37 | /// Non-destructive equality check of the top two items of the stack. At this 38 | /// low a level, there is no type system, so checking equality of disparate 39 | /// primitive types will panic. 40 | pub fn EQ(runtime: *Runtime) !void { 41 | const peek = try runtime.stack_peek_pair(); 42 | 43 | if (peek.far) |bottom| { 44 | _ = try peek.near.assert_same_kind_as(bottom); 45 | try runtime.stack_push_bool(true); 46 | return; 47 | } 48 | 49 | return StackManipulationError.Underflow; 50 | } 51 | 52 | test "EQ" { 53 | var runtime = try Runtime.init(testAllocator); 54 | defer runtime.deinit(); 55 | try runtime.stack_push_uint(1); 56 | // Can't compare with just one Object on the Stack. 57 | try expectError(StackManipulationError.Underflow, EQ(&runtime)); 58 | try runtime.stack_push_uint(1); 59 | // 1 == 1, revelatory, truly. 60 | try EQ(&runtime); 61 | try expect((try runtime.stack_peek_pair()).near.*.Boolean); 62 | // Now compare that boolean to the UnsignedInt... or don't, preferably. 63 | try expectError(InternalError.TypeError, EQ(&runtime)); 64 | } 65 | 66 | /// @DROP ( @1 -> nothing ) 67 | pub fn DROP(runtime: *Runtime) !void { 68 | try runtime.stack_wrangle(.DropTopObject); 69 | } 70 | 71 | /// @DUP ( @1 -> @1 ) 72 | pub fn DUP(runtime: *Runtime) !void { 73 | try runtime.stack_wrangle(.DuplicateTopObject); 74 | } 75 | 76 | /// @2DUPSHUF ( @2 @1 -> @2 @1 @2 @1 ) 77 | pub fn TWODUPSHUF(runtime: *Runtime) !void { 78 | try runtime.stack_wrangle(.DuplicateTopTwoObjectsShuffled); 79 | } 80 | 81 | /// @LIT ( @1 -> Word ) 82 | /// 83 | /// Wraps any value type in an anonymous word which will return that value when 84 | /// called. Generally useful when defining words which need to refer to 85 | /// numbers, strings, symbols, etc. at runtime. 86 | /// 87 | /// Used to be called @HEAPWRAP, which might hint at why it's implemented the 88 | /// way it is. 89 | pub fn LIT(runtime: *Runtime) !void { 90 | // TODO: Should these return Bounded versions instead, since we inherently 91 | // already know the word's return value? 92 | const banished = try runtime.stack_pop_to_heap(); 93 | const word = try runtime.word_from_heaplit_impl(banished, switch (banished.*) { 94 | // TODO, maybe this should be unreachable, since opaques are meant more 95 | // for things like FFI storage rather than raw, Gale-side bit access. 96 | .Opaque => @panic("unimplemented"), 97 | 98 | .Array => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedArray) }, 99 | .Boolean => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedBoolean) }, 100 | .Float => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedFloat) }, 101 | .SignedInt => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedSignedInt) }, 102 | .UnsignedInt => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedUnsignedInt) }, 103 | .String => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedString) }, 104 | .Symbol => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedSymbol) }, 105 | .Word => .{ .Declared = runtime.get_well_known_word_signature(.NullarySingleUnboundedWord) }, 106 | }); 107 | try runtime.stack_push_raw_word(word); 108 | } 109 | 110 | test "LIT" { 111 | var runtime = try Runtime.init(testAllocator); 112 | defer runtime.deinit_guard_for_empty_stack(); 113 | 114 | // First, push an UnsignedInt literal onto the stack 115 | try runtime.stack_push_uint(1); 116 | 117 | // Create and yank a HeapLit word from that UnsignedInt 118 | try LIT(&runtime); 119 | var lit_word = try runtime.stack_pop(); 120 | defer lit_word.deinit(testAllocator); 121 | 122 | // That word should have been the only thing on the stack 123 | try expectError(StackManipulationError.Underflow, runtime.stack_pop()); 124 | 125 | // Now, run the word - three times, for kicks... 126 | try runtime.run_word(lit_word.Word); 127 | try runtime.run_word(lit_word.Word); 128 | try runtime.run_word(lit_word.Word); 129 | 130 | // ...and assert that the UnsignedInt was placed back onto the stack all 131 | // three times. 132 | const top_three = try runtime.stack_pop_trio(); 133 | try expectEqual(@as(usize, 1), top_three.near.UnsignedInt); 134 | try expectEqual(@as(usize, 1), top_three.far.UnsignedInt); 135 | try expectEqual(@as(usize, 1), top_three.farther.UnsignedInt); 136 | } 137 | 138 | /// @SWAP ( @2 @1 -> @2 @1 ) 139 | pub fn SWAP(runtime: *Runtime) !void { 140 | try runtime.stack_wrangle(.SwapTopTwoObjects); 141 | } 142 | 143 | test { 144 | std.testing.refAllDecls(@This()); 145 | } 146 | -------------------------------------------------------------------------------- /lib/gale/object.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const Allocator = std.mem.Allocator; 16 | 17 | const InternalError = @import("./internal_error.zig").InternalError; 18 | const Types = @import("./types.zig"); 19 | 20 | /// Within our Stack we can store a few primitive types: 21 | pub const Object = union(enum) { 22 | const Self = @This(); 23 | 24 | // TODO: primitive types should carry a preceding nullable pointer to their 25 | // respective Shape. For performance, null pointers here will represent 26 | // their base Shape, so [null, 8 as usize] is known to be an UnsignedInt, 27 | // but [Meters, 8 as usize] is known to be a Meters type of UnsignedInt 28 | 29 | Array: *Types.HeapedArray, 30 | Boolean: bool, 31 | Float: f64, 32 | /// Opaque represents a blob of memory that is left to userspace to manage 33 | /// manually. TODO more docs here. 34 | Opaque: *Types.HeapedOpaque, 35 | SignedInt: isize, 36 | String: *Types.HeapedString, 37 | Symbol: *Types.HeapedSymbol, 38 | UnsignedInt: usize, 39 | Word: *Types.HeapedWord, 40 | 41 | pub fn deinit(self: *Self, alloc: Allocator) void { 42 | switch (self.*) { 43 | .Array => |inner| { 44 | // First, we need to deref and kill all the objects stored 45 | // in this array, garbage collecting the inner contents as 46 | // necessary. 47 | // 48 | // Using ? here because if we have an Rc with no contents at 49 | // this point, something has gone horribly, horribly wrong, and 50 | // panicking the thread is appropriate. 51 | for (inner.value.?.items) |_it| { 52 | var it = _it; 53 | it.deinit(alloc); 54 | } 55 | 56 | // Now we can toss this Object and the ArrayList stored within. 57 | // Passing alloc here is necessary by type signature only; it 58 | // won't be used (since ArrayList is a ManagedStruct). 59 | _ = inner.decrement_and_prune(.DeinitInner, alloc); 60 | }, 61 | .Boolean, .Float, .SignedInt, .UnsignedInt => {}, 62 | .String, .Symbol => |inner| { 63 | _ = inner.decrement_and_prune(.FreeInnerDestroySelf, alloc); 64 | }, 65 | // TODO: how to handle this? 66 | .Opaque => unreachable, 67 | .Word => |inner| { 68 | _ = inner.decrement_and_prune(.DeinitInnerWithAllocDestroySelf, alloc); 69 | }, 70 | } 71 | } 72 | 73 | /// Indicate another reference to the underlying data has been made in 74 | /// userspace, which is a no-op for "unboxed" types, and increments the 75 | /// internal `strong_count` for the "boxed"/managed types. Returns self 76 | /// after such internal mutations have been made, mostly for chaining 77 | /// ergonomics. 78 | pub fn ref(self: Self) !Self { 79 | switch (self) { 80 | .Array => |rc| try rc.increment(), 81 | .Boolean, .Float, .SignedInt, .UnsignedInt => {}, 82 | .String => |rc| try rc.increment(), 83 | .Symbol => |rc| try rc.increment(), 84 | .Opaque => |rc| try rc.increment(), 85 | .Word => |rc| try rc.increment(), 86 | } 87 | 88 | return self; 89 | } 90 | 91 | /// Raise an `InternalError.TypeError` if this object is not the same primitive 92 | /// kind as `other`. 93 | /// 94 | /// Returns `self` after this check to allow for chaining. 95 | pub fn assert_same_kind_as(self: *Self, other: *Self) InternalError!*Self { 96 | if (std.meta.activeTag(self.*) != std.meta.activeTag(other.*)) { 97 | return InternalError.TypeError; 98 | } 99 | 100 | return self; 101 | } 102 | }; 103 | 104 | test { 105 | std.testing.refAllDecls(@This()); 106 | } 107 | -------------------------------------------------------------------------------- /lib/gale/parsed_word.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const expect = std.testing.expect; 16 | const expectApproxEqAbs = std.testing.expectApproxEqAbs; 17 | const expectEqual = std.testing.expectEqual; 18 | const expectEqualStrings = std.testing.expectEqualStrings; 19 | const expectError = std.testing.expectError; 20 | 21 | const InternalError = @import("./internal_error.zig").InternalError; 22 | const helpers = @import("./helpers.zig"); 23 | 24 | /// Commas can be placed before and/or after simple word lookups to modify the 25 | /// behavior of the stack. These convenience modifiers serve to alleviate 26 | /// common sources of stack shuffling pain in other stack-based languages. 27 | /// 28 | /// - Commas before the word indicate that the top object of the stack should be 29 | /// stashed (popped off) during lookup and execution of this word, and 30 | /// restored underneath the effects of the word (unless a trailing comma is 31 | /// additionally used, see below). 32 | /// 33 | /// Given `: say-hello ( String -> String )` which returns "Hello, {name}", 34 | /// using the top object off the stack (which must be a String) to fill 35 | /// `name`, and given a Stack with two elements, "World" on top of "Josh", 36 | /// `,say-hello` would result in a stack with "Hello, Josh" on top of "World". 37 | /// 38 | /// - Commas after the word indicate that the object on top of the stack prior to 39 | /// execution of the word should be moved to the top of the stack after 40 | /// execution (regardless of how many objects the word places on the stack). 41 | /// This can only be used with purely additive words (those using `<-` stack 42 | /// signatures) unless combined with a leading comma, as its use with words 43 | /// free to consume stack objects would be some combination of ambiguous, 44 | /// confusing, and inelegant. 45 | /// 46 | /// Given the Prelude's `Equatable/eq` word, which adds a Boolean to the stack 47 | /// reflecting the equality of the top two objects on the stack, and given a 48 | /// stack with "foo" on top of "bar", `Equatable/eq,` would result in a stack 49 | /// with "foo" on top of `Boolean.False` on top of "bar", functionally 50 | /// equivalent to having run `Equatable/eq swap`, though the stack-juggling 51 | /// sanity restored by this operator scales with the number of objects a word 52 | /// gives. 53 | /// 54 | /// - The use of both comma operators on the same word stashes whatever is on 55 | /// top of the stack, looks up and runs the word in question, and restores 56 | /// that object to the top of the stack after the word has completed. There 57 | /// is no restriction of the word being purely-additive as with the 58 | /// trailing-comma-only case above. 59 | /// 60 | /// Given the same `say-hello` word as in the leading comma example, and given 61 | /// the same two Strings on the stack, `,say-hello,` would result in a stack 62 | /// with "World" on top of "Hello, Josh". 63 | /// 64 | /// Given the same `Equatable/eq` situation as in the trailing comma example, 65 | /// and given the same two Strings on the stack, `,Equatable/eq` would 66 | /// underflow, as only one object would be visible on the stack. Presuming 67 | /// the stack were instead "foo" on top of "bar" on top of "baz", 68 | /// `,Equatable/eq,` would result in `Boolean.False` being inserted between 69 | /// "foo" and "bar". 70 | /// 71 | /// Commas are forbidden from use anywhere in word names to reduce confusion 72 | /// (given that `,,,some,thing,,` would be the stashing and hoisting form of 73 | /// `,,some,thing,`, which is comically difficult to make sense of while 74 | /// skimming source code). 75 | /// 76 | /// Use of leading and/or trailing commas on any word with an empty stack 77 | /// always results in an underflow, and should be forbidden by analysis tools. 78 | // TODO: ^ should be moved to proper language docs somewhere, as it's only 79 | // somewhat related to `CHAR_COMMA` and those docs are actually good. Nobody 80 | // will ever find them buried deep in a string parser here. 81 | const CHAR_COMMA = helpers.CHAR_COMMA; 82 | 83 | const CHAR_DOT = helpers.CHAR_DOT; 84 | const EMPTY_STRING = helpers.EMPTY_STRING; 85 | 86 | /// While some FORTHs choose to use s" "s as immediate mode words and then 87 | /// slurp up the character stream in between to use as the body of the string, 88 | /// and while that would certainly be an *easier* and more *consistent* thing 89 | /// to do in the language spec, it's ugly and a horrible user experience, so 90 | /// instead, " (UTF-8 0x22) is one of the few reserved characters for use 91 | const STRING_WORD_DELIMITER = helpers.CHAR_QUOTE_DBL; 92 | 93 | /// Borrowing an idea from Ruby, Elixir, and others, identifiers starting with 94 | /// a single colon (:) are reserved for denoting raw identifiers, generally 95 | /// used for defining names of low-level things (say, Shapes and their 96 | /// members). 97 | const SYMBOL_WORD_DELIMITER = helpers.CHAR_COLON; 98 | 99 | /// Finally, borrowing an idea from countless languages, identifiers starting 100 | /// with ampersands are also reserved: the & will be dropped, and the remaining 101 | /// characters will be used as the name of the thing to look up following the 102 | /// exact same rules as we'd normally use for execution flow, but rather than 103 | /// calling the Word, we'll return a Reference to it. 104 | /// 105 | /// Referencing a primitive type, for example with '1, is redundant, and will 106 | /// still place the primitive type onto the Stack. 107 | const REF_WORD_DELIMITER = helpers.CHAR_AMPER; 108 | 109 | pub const ParsedWord = union(enum) { 110 | const Self = @This(); 111 | 112 | pub const SimpleWordReference = struct { 113 | name: []const u8, 114 | semantics: packed struct { 115 | stash_before_lookup: bool, 116 | hoist_after_result: bool, 117 | }, 118 | }; 119 | 120 | String: []const u8, 121 | Symbol: []const u8, 122 | Ref: []const u8, 123 | NumFloat: f64, 124 | SignedInt: isize, 125 | UnsignedInt: usize, 126 | Simple: SimpleWordReference, 127 | 128 | /// In any event of ambiguity, input strings are parsed in the following 129 | /// order of priority: 130 | /// 131 | /// - Empty input (returns EmptyWord error) 132 | /// - Strings 133 | /// - Symbols 134 | /// - Ref strings 135 | /// - Floats 136 | /// - Ints 137 | /// - Assumed actual words ("Simples") 138 | pub fn from_input(input: []const u8) !Self { 139 | if (input.len == 0 or std.mem.eql(u8, EMPTY_STRING, input)) { 140 | return InternalError.EmptyWord; 141 | } 142 | 143 | // TODO: This presumes that string quote handling actually happens a 144 | // level above (read: that the word splitter understands that "these 145 | // are all one word"), which probably isn't the cleanest design 146 | if ((input[0] == STRING_WORD_DELIMITER) and 147 | (input[input.len - 1] == STRING_WORD_DELIMITER)) 148 | { 149 | return ParsedWord{ .String = input[1 .. input.len - 1] }; 150 | } 151 | 152 | if (input[0] == SYMBOL_WORD_DELIMITER) { 153 | return ParsedWord{ .Symbol = input[1..input.len] }; 154 | } 155 | 156 | if (input[0] == REF_WORD_DELIMITER) { 157 | return ParsedWord{ .Ref = input[1..input.len] }; 158 | } 159 | 160 | if (std.mem.indexOfScalar(u8, input, CHAR_DOT) != null) { 161 | if (std.fmt.parseFloat(f64, input) catch null) |parsed| { 162 | return ParsedWord{ .NumFloat = parsed }; 163 | } 164 | } 165 | 166 | if (input.len > 1) { 167 | const first_char = input[0]; 168 | if ((first_char == '+' or first_char == '-') and 169 | (input[1] >= '0' and input[1] <= '9')) 170 | { 171 | const start_idx: usize = if (first_char == '-') 0 else 1; 172 | if (std.fmt.parseInt(isize, input[start_idx..], 10) catch null) |parsed| { 173 | return ParsedWord{ .SignedInt = parsed }; 174 | } 175 | } 176 | } 177 | 178 | if (std.fmt.parseInt(usize, input, 10) catch null) |parsed| { 179 | return ParsedWord{ .UnsignedInt = parsed }; 180 | } 181 | 182 | if (input.len == 1 and input[0] == CHAR_COMMA) { 183 | return InternalError.InvalidWordName; 184 | } 185 | 186 | const leading_comma = input[0] == CHAR_COMMA; 187 | const trailing_comma = input[input.len - 1] == CHAR_COMMA; 188 | 189 | if ((input.len == 1 and leading_comma) or 190 | (input.len == 2 and leading_comma and trailing_comma)) 191 | { 192 | return InternalError.InvalidWordName; 193 | } 194 | 195 | const name_slice_start: usize = if (leading_comma) 1 else 0; 196 | const name_slice_end: usize = if (trailing_comma) input.len - 1 else input.len; 197 | const name_slice = input[name_slice_start..name_slice_end]; 198 | 199 | for (name_slice) |chr| if (chr == CHAR_COMMA) return InternalError.InvalidWordName; 200 | 201 | return ParsedWord{ .Simple = .{ 202 | .semantics = .{ 203 | .stash_before_lookup = leading_comma, 204 | .hoist_after_result = trailing_comma, 205 | }, 206 | .name = name_slice, 207 | } }; 208 | } 209 | 210 | fn parseInputAsSignedInt(input: []const u8) ?ParsedWord { 211 | if (std.fmt.parseInt(isize, input, 10) catch null) |parsed| { 212 | return ParsedWord{ .SignedInt = parsed }; 213 | } 214 | 215 | return null; 216 | } 217 | 218 | test "errors on empty words" { 219 | try expectError(InternalError.EmptyWord, from_input(EMPTY_STRING)); 220 | } 221 | 222 | test "parses strings: basic" { 223 | const result = (try from_input( 224 | "\"I don't know me and you don't know you\"", 225 | )).String; 226 | 227 | try expectEqualStrings( 228 | "I don't know me and you don't know you", 229 | result, 230 | ); 231 | } 232 | 233 | test "parses strings: unicodey" { 234 | const result = (try from_input("\"yeee 🐸☕ hawwww\"")).String; 235 | try expectEqualStrings("yeee 🐸☕ hawwww", result); 236 | } 237 | 238 | test "parses symbols: basic" { 239 | const result = (try from_input(":Testable")).Symbol; 240 | try expectEqualStrings("Testable", result); 241 | } 242 | 243 | test "parses symbols: unicodey" { 244 | const result = (try from_input(":🐸☕")).Symbol; 245 | try expectEqualStrings("🐸☕", result); 246 | } 247 | 248 | test "parses refs: basic" { 249 | const result = (try from_input("&Testable")).Ref; 250 | try expectEqualStrings("Testable", result); 251 | } 252 | 253 | test "parses floats: bare" { 254 | try expectApproxEqAbs( 255 | @as(f64, 3.14), 256 | (try from_input("3.14")).NumFloat, 257 | @as(f64, 0.001), 258 | ); 259 | try expectApproxEqAbs( 260 | @as(f64, 0.0), 261 | (try from_input("0.0")).NumFloat, 262 | @as(f64, 0.0000001), 263 | ); 264 | try expectApproxEqAbs( 265 | @as(f64, 0.0), 266 | (try from_input("0.000000000000000")).NumFloat, 267 | @as(f64, 0.0000001), 268 | ); 269 | } 270 | 271 | test "parses ints: bare" { 272 | try expectEqual(@as(usize, 420), (try from_input("420")).UnsignedInt); 273 | } 274 | 275 | test "parses ints: prefixes" { 276 | try expectEqual(@as(isize, -420), (try from_input("-420")).SignedInt); 277 | try expectEqual(@as(isize, 420), (try from_input("+420")).SignedInt); 278 | } 279 | 280 | test "parses simple word incantations" { 281 | const result = (try from_input("@BEFORE_WORD")).Simple; 282 | try expectEqualStrings("@BEFORE_WORD", result.name); 283 | try expect(result.semantics.stash_before_lookup == false); 284 | try expect(result.semantics.hoist_after_result == false); 285 | } 286 | 287 | test "parses word names: stashing mode" { 288 | const result = (try from_input(",@BEFORE_WORD")).Simple; 289 | try expectEqualStrings("@BEFORE_WORD", result.name); 290 | try expect(result.semantics.stash_before_lookup); 291 | try expect(result.semantics.hoist_after_result == false); 292 | } 293 | 294 | test "parses word names: hoisting mode" { 295 | const result = (try from_input("@BEFORE_WORD,")).Simple; 296 | try expectEqualStrings("@BEFORE_WORD", result.name); 297 | try expect(result.semantics.stash_before_lookup == false); 298 | try expect(result.semantics.hoist_after_result); 299 | } 300 | 301 | test "parses word names: stashing+hoisting mode" { 302 | const result = (try from_input(",@BEFORE_WORD,")).Simple; 303 | try expectEqualStrings("@BEFORE_WORD", result.name); 304 | try expect(result.semantics.stash_before_lookup); 305 | try expect(result.semantics.hoist_after_result); 306 | } 307 | 308 | test "word names must not contain internal commas" { 309 | try expectError(InternalError.InvalidWordName, from_input(",")); 310 | try expectError(InternalError.InvalidWordName, from_input(",,")); 311 | try expectError(InternalError.InvalidWordName, from_input(",,,")); 312 | try expectError(InternalError.InvalidWordName, from_input("foo,bar")); 313 | try expectError(InternalError.InvalidWordName, from_input(",,foo")); 314 | try expectError(InternalError.InvalidWordName, from_input(",,foo,")); 315 | try expectError(InternalError.InvalidWordName, from_input(",,foo,,")); 316 | try expectError(InternalError.InvalidWordName, from_input(",foo,,")); 317 | try expectError(InternalError.InvalidWordName, from_input("foo,,")); 318 | } 319 | }; 320 | 321 | test { 322 | std.testing.refAllDecls(@This()); 323 | } 324 | -------------------------------------------------------------------------------- /lib/gale/rc.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const Allocator = std.mem.Allocator; 16 | const testAllocator: Allocator = std.testing.allocator; 17 | const expect = std.testing.expect; 18 | 19 | const builtin = @import("builtin"); 20 | 21 | const InternalError = @import("./internal_error.zig").InternalError; 22 | const Types = @import("./types.zig"); 23 | 24 | // TODO: allow override in build.zig 25 | const RC_ALLOW_INIT_GT0: bool = false; 26 | 27 | pub fn Rc(comptime T: type) type { 28 | return struct { 29 | const no_valid_innerkind_msg = "Could not determine an InnerKind for value's type: " ++ @typeName(T); 30 | const inner_kind = switch (@typeInfo(T)) { 31 | .Pointer => |pointer| switch (pointer.size) { 32 | .One => .SinglePointer, 33 | .Slice => .SlicePointer, 34 | else => @compileError(no_valid_innerkind_msg), 35 | }, 36 | .Struct => |container| strct: { 37 | // TODO: See if this is the only differentiator worth caring 38 | // about, or if we should also factor things like the name of 39 | // the field in. Or maybe we need a wrapper type rather than 40 | // metaprogramming magic, who knows. 41 | for (container.fields) |field| if (field.field_type == Allocator) { 42 | break :strct .ManagedStruct; 43 | }; 44 | break :strct .Struct; 45 | }, 46 | else => @compileError(no_valid_innerkind_msg), 47 | }; 48 | 49 | pub const PruneMode = switch (inner_kind) { 50 | .SinglePointer => enum { 51 | DestroyInner, 52 | DestroyInnerAndSelf, 53 | }, 54 | .SlicePointer => enum { 55 | FreeInner, 56 | FreeInnerDestroySelf, 57 | }, 58 | .Struct => enum { 59 | DeinitInnerWithAlloc, 60 | DeinitInnerWithAllocDestroySelf, 61 | }, 62 | .ManagedStruct => enum { DeinitInner }, 63 | else => @compileError("No valid PruneModes exist for value's type: " ++ @typeName(T)), 64 | }; 65 | 66 | const Self = @This(); 67 | const RefCount = std.atomic.Atomic(u16); 68 | 69 | strong_count: RefCount, 70 | value: ?T, 71 | 72 | /// Initialize an Rc(T) with no references. This is almost always the 73 | /// correct default, but if you're absolutely sure it's not, see 74 | /// `init_referenced`. 75 | pub fn init(value: ?T) Self { 76 | return Self{ 77 | .strong_count = RefCount.init(0), 78 | .value = value, 79 | }; 80 | } 81 | 82 | /// Initialize an Rc(T) which is assumed to already have a reference. 83 | /// This should almost never be used directly outside of tests, as most 84 | /// Stack and/or Object operations assume an Rc(_) starts with no 85 | /// references (since strong_count really refers to the number of 86 | /// gale-side references, and is not meant to be a generic zig garbage 87 | /// collector). 88 | pub fn init_referenced(value: ?T) Self { 89 | if (!builtin.is_test and !RC_ALLOW_INIT_GT0) { 90 | @compileError(@typeName(Self) ++ ".init_referenced called outside of unit tests (override with build arg RC_ALLOW_INIT_GT0)"); 91 | } 92 | 93 | return Self{ 94 | .strong_count = RefCount.init(1), 95 | .value = value, 96 | }; 97 | } 98 | 99 | /// Return whether or not this object is considered "dead", in that it 100 | /// is no longer referenced and contains only a null value. This is 101 | /// *not* the same logic used by `decrement_and_prune`, but can be used 102 | /// to build multi-step prune-then-destroy garbage collection passes. 103 | pub fn dead(self: *Self) bool { 104 | const no_refs = self.strong_count.load(.Acquire) == 0; 105 | const no_data = self.value == null; 106 | 107 | if (no_refs and !no_data) { 108 | std.debug.panic("partially-dead Rc@{d}: no refs, but data still exists", .{&self}); 109 | } 110 | 111 | if (!no_refs and no_data) { 112 | std.debug.panic("improperly killed Rc@{d}: data has been wiped, but references remain", .{&self}); 113 | } 114 | 115 | return no_refs and no_data; 116 | } 117 | 118 | /// Increment the number of references to this Rc. 119 | pub fn increment(self: *Self) InternalError!void { 120 | if (self.value == null) return InternalError.AttemptedResurrectionOfExhaustedRc; 121 | _ = self.strong_count.fetchAdd(1, .Monotonic); 122 | } 123 | 124 | /// Returned boolean reflects the livelihood of the object after this 125 | /// decrement: if false, it is assumed to be safe for callers to free 126 | /// the underlying data. 127 | // TODO: flip this boolean: the ergonomics are stupid after a while of 128 | // actually using this, and there's constant NOT-ing being done to 129 | // coerce this back into the "right" question's answer ("is this object 130 | // freeable?" "yes"). 131 | pub fn decrement(self: *Self) bool { 132 | // Release ensures code before unref() happens-before the count is 133 | // decremented as dropFn could be called by then. 134 | if (self.strong_count.fetchSub(1, .Release) == 1) { 135 | // Acquire ensures count decrement and code before previous 136 | // unrefs()s happens-before we null the field. 137 | self.strong_count.fence(.Acquire); 138 | self.value = null; 139 | return false; 140 | } 141 | 142 | return true; 143 | } 144 | 145 | /// Decrement strong count by one and prune data in varying ways. 146 | /// Returns whether pruning was done: if true, the inner data of this 147 | /// Rc has been freed and will segfault if accessed, and depending on 148 | /// PruneMode, the Rc itself may no longer be valid, either. 149 | /// 150 | /// In general, PruneMode.*Self are the correct modes if this Rc is 151 | /// heap-allocated with the Runtime's allocator, and the other options 152 | /// are good fits for stack-allocated Rcs, or those allocated by 153 | /// allocators the Runtime does not own (perhaps, HeapMap or 154 | /// ArrayList), for example when clearing inner data from a 155 | /// valueIterator().next().value - the pointer itself is owned by the 156 | /// ArrayList, but the inner datastructures are generally gale-owned 157 | /// and will leak if not torn down correctly. 158 | /// 159 | /// Since PruneModes are comptime-generated enums, it is not possible 160 | /// to use an outright invalid mode for the underlying value's type, 161 | /// say, .DestroyInner on a slice pointer (for which .FreeInner would 162 | /// be the correct instruction). 163 | pub fn decrement_and_prune(self: *Self, prune_mode: PruneMode, alloc: Allocator) bool { 164 | var inner = self.value.?; 165 | const is_dead = !self.decrement(); 166 | 167 | // TODO: since strings and symbols are interned to Runtime, right 168 | // now they will just leak until the Runtime is deinit()-ed. Unsure 169 | // whether this method should go poke Runtime to clean up its 170 | // intern table (and then almost certainly call this function 171 | // itself), or if Runtime should have some sort of mark-and-sweep 172 | // process that periodically finds interned strings and symbols for 173 | // which it holds the only reference. I'm leaving this comment 174 | // *here* because it's the most localized place I can think to put 175 | // it, the top of Rc() is probably not a great fit. 176 | 177 | // TODO: handle nulls safely, which will require plumbing an 178 | // InternalError up many stacks... 179 | if (is_dead) switch (inner_kind) { 180 | .SinglePointer => switch (prune_mode) { 181 | .DestroyInner => alloc.destroy(inner), 182 | .DestroyInnerAndSelf => { 183 | alloc.destroy(inner); 184 | alloc.destroy(self); 185 | }, 186 | }, 187 | .SlicePointer => switch (prune_mode) { 188 | .FreeInner => alloc.free(inner), 189 | .FreeInnerDestroySelf => { 190 | alloc.free(inner); 191 | alloc.destroy(self); 192 | }, 193 | }, 194 | .Struct => switch (prune_mode) { 195 | .DeinitInnerWithAlloc => inner.deinit(alloc), 196 | .DeinitInnerWithAllocDestroySelf => { 197 | inner.deinit(alloc); 198 | alloc.destroy(self); 199 | }, 200 | }, 201 | .ManagedStruct => switch (prune_mode) { 202 | .DeinitInner => inner.deinit(), 203 | }, 204 | else => unreachable, 205 | }; 206 | 207 | return is_dead; 208 | } 209 | }; 210 | } 211 | 212 | test "Rc(u8): simple set, increments, decrements, and prune" { 213 | const SharedStr = Types.HeapedString; 214 | const hello_world = "Hello World!"; 215 | var str = try testAllocator.alloc(u8, hello_world.len); 216 | std.mem.copy(u8, str[0..], hello_world); 217 | var shared_str = try testAllocator.create(SharedStr); 218 | shared_str.* = SharedStr.init_referenced(str); 219 | 220 | try std.testing.expectEqualStrings(hello_world, shared_str.value.?); 221 | try expect(shared_str.strong_count.load(.Acquire) == 1); 222 | try shared_str.increment(); 223 | try expect(shared_str.strong_count.load(.Acquire) == 2); 224 | try expect(!shared_str.decrement_and_prune(.FreeInnerDestroySelf, testAllocator)); 225 | try expect(shared_str.strong_count.load(.Acquire) == 1); 226 | 227 | // This last assertion is testing a lot of things in one swing, but is a 228 | // realistic usecase: given that we have one remaining reference, decrement 229 | // *again*, leaving us with no remaining references, and collect garbage by 230 | // freeing both the underlying u8 slice *and* the Rc itself. Thus, this 231 | // assertion is only part of the test's succeeding, the rest comes from 232 | // Zig's GeneralPurposeAllocator not warning us about any leaked memory 233 | // (which fails the tests at a higher level). 234 | try expect(shared_str.decrement_and_prune(.FreeInnerDestroySelf, testAllocator)); 235 | } 236 | 237 | test { 238 | std.testing.refAllDeclsRecursive(@This()); 239 | } 240 | -------------------------------------------------------------------------------- /lib/gale/runtime.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const Allocator = std.mem.Allocator; 16 | const testAllocator: Allocator = std.testing.allocator; 17 | const expectApproxEqAbs = std.testing.expectApproxEqAbs; 18 | const expectEqual = std.testing.expectEqual; 19 | const expectEqualStrings = std.testing.expectEqualStrings; 20 | const expectError = std.testing.expectError; 21 | 22 | const builtin = @import("builtin"); 23 | 24 | const _object = @import("./object.zig"); 25 | const _stack = @import("./stack.zig"); 26 | const _word = @import("./word.zig"); 27 | 28 | const helpers = @import("./helpers.zig"); 29 | const well_known_entities = @import("./well_known_entities.zig"); 30 | 31 | const CompoundImplementation = _word.CompoundImplementation; 32 | const HeapLitImplementation = _word.HeapLitImplementation; 33 | const InternalError = @import("./internal_error.zig").InternalError; 34 | const Object = _object.Object; 35 | const ParsedWord = @import("./parsed_word.zig").ParsedWord; 36 | const PrimitiveImplementation = _word.PrimitiveImplementation; 37 | const Shape = @import("./shape.zig").Shape; 38 | const Stack = _stack.Stack; 39 | const StackManipulationError = _stack.StackManipulationError; 40 | const Types = @import("./types.zig"); 41 | const Word = _word.Word; 42 | const WordList = @import("./word_list.zig").WordList; 43 | const WordMap = @import("./word_map.zig").WordMap; 44 | const WordSignature = @import("./word_signature.zig").WordSignature; 45 | const WellKnownShape = well_known_entities.WellKnownShape; 46 | const WellKnownShapeStorage = well_known_entities.WellKnownShapeStorage; 47 | const WellKnownSignature = well_known_entities.WellKnownSignature; 48 | const WellKnownSignatureStorage = well_known_entities.WellKnownSignatureStorage; 49 | 50 | pub const Runtime = struct { 51 | const Self = @This(); 52 | 53 | pub const InterpreterMode = enum(u8) { 54 | Exec = 0, 55 | Symbol = 1, 56 | Ref = 2, 57 | }; 58 | 59 | const PrivateSpace = struct { 60 | interpreter_mode: InterpreterMode, 61 | 62 | pub fn init() PrivateSpace { 63 | return PrivateSpace{ 64 | .interpreter_mode = InterpreterMode.Exec, 65 | }; 66 | } 67 | }; 68 | 69 | /// These characters separate identifiers, and can broadly be defined as 70 | /// "typical ASCII whitespace": UTF-8 codepoints 0x20 (space), 0x09 (tab), 71 | /// and 0x0A (newline). This technically leaves the door open to 72 | /// tricky-to-debug behaviors like using 0xA0 (non-breaking space) as 73 | /// identifiers. With great power comes great responsibility. Don't be 74 | /// silly. 75 | const WORD_SPLITTING_CHARS: [3]u8 = .{ 76 | helpers.CHAR_NEWLINE, 77 | helpers.CHAR_SPACE, 78 | helpers.CHAR_TAB, 79 | }; 80 | 81 | /// Speaking of Words: WORD_BUF_LEN is how big of a buffer we're willing to 82 | /// allocate to store words as they're input. We have to draw a line 83 | /// _somewhere_, and since 1KB of RAM is beyond feasible to allocate on 84 | /// most systems I'd foresee writing gale for, that's the max word length 85 | /// until I'm convinced otherwise. This should be safe to change and the 86 | /// implementation will scale proportionally. 87 | // 88 | // TODO: configurable in build.zig 89 | const WORD_BUF_LEN = 1024; 90 | 91 | // TODO: configurable in build.zig 92 | const DICTIONARY_DEFAULT_SIZE = 4096; 93 | 94 | // TODO: configurable in build.zig 95 | const SYMBOL_POOL_DEFAULT_SIZE = 4096; 96 | 97 | // TODO: configurable in build.zig 98 | const SIGNATURE_POOL_DEFAULT_SIZE = 8192; 99 | 100 | /// All symbols are interned by their raw "string" contents and stored 101 | /// behind a typical garbage collection structure (Rc([]u8)) for later 102 | /// pulling onto a stack. 103 | const SymbolPool = std.StringHashMap(Types.HeapedSymbol); 104 | 105 | // TODO: use HashSet if https://github.com/ziglang/zig/issues/6919 ever 106 | // moves 107 | const WordSignaturePool = std.hash_map.HashMap( 108 | WordSignature, 109 | void, 110 | struct { 111 | pub const eql = std.hash_map.getAutoEqlFn(WordSignature, @This()); 112 | pub fn hash(ctx: @This(), key: WordSignature) u64 { 113 | _ = ctx; 114 | if (comptime std.meta.trait.hasUniqueRepresentation(WordSignature)) { 115 | return std.hash.Wyhash.hash(0, std.mem.asBytes(&key)); 116 | } else { 117 | var hasher = std.hash.Wyhash.init(0); 118 | std.hash.autoHashStrat(&hasher, key, .DeepRecursive); 119 | return hasher.final(); 120 | } 121 | } 122 | }, 123 | std.hash_map.default_max_load_percentage, 124 | ); 125 | 126 | fn GetOrPutResult(comptime T: type) type { 127 | return struct { 128 | value_ptr: *T, 129 | found_existing: bool, 130 | }; 131 | } 132 | 133 | alloc: Allocator, 134 | dictionary: WordMap, 135 | private_space: PrivateSpace, 136 | stack: *Stack, 137 | symbols: SymbolPool, 138 | signatures: WordSignaturePool, 139 | well_known_shapes: WellKnownShapeStorage, 140 | well_known_signatures: WellKnownSignatureStorage, 141 | 142 | pub fn init(alloc: Allocator) !Self { 143 | var dictionary = WordMap.init(alloc); 144 | try dictionary.ensureTotalCapacity(DICTIONARY_DEFAULT_SIZE); 145 | 146 | var symbol_pool = SymbolPool.init(alloc); 147 | try symbol_pool.ensureTotalCapacity(SYMBOL_POOL_DEFAULT_SIZE); 148 | 149 | var signature_pool = WordSignaturePool.init(alloc); 150 | try signature_pool.ensureTotalCapacity(SIGNATURE_POOL_DEFAULT_SIZE); 151 | 152 | var rt = Self{ 153 | .alloc = alloc, 154 | .dictionary = dictionary, 155 | .private_space = PrivateSpace.init(), 156 | .stack = try Stack.init(alloc, null), 157 | .symbols = symbol_pool, 158 | .signatures = signature_pool, 159 | .well_known_shapes = well_known_entities.shape_storage(), 160 | .well_known_signatures = well_known_entities.signature_storage(), 161 | }; 162 | 163 | try well_known_entities.populate(&rt); 164 | 165 | return rt; 166 | } 167 | 168 | pub fn deinit(self: *Self) void { 169 | // First, nuke everything on the stack using this horribly named method 170 | // (TODO for the love of god find better names for these things). 171 | self.stack.deinit_from_bottom(); 172 | self.deinit_shared(); 173 | } 174 | 175 | /// The rest of the deinit() sequence, shared between the standard deinit() 176 | /// and the test-mode-only deinit_guard_for_empty_stack(). 177 | fn deinit_shared(self: *Self) void { 178 | // Now, we need to nuke all defined words, which is a bit fidgety since 179 | // they're referenced by their symbol identifiers which themselves may 180 | // need to be garbage collected in this process. 181 | var dictionary_iter = self.dictionary.iterator(); 182 | while (dictionary_iter.next()) |entry| { 183 | // Drop our reference to the symbol naming this WordList (and free 184 | // the underlying u8 slice if appropriate). 185 | _ = entry.key_ptr.*.decrement_and_prune(.FreeInnerDestroySelf, self.alloc); 186 | // Now defer to WordList.deinit to clean its own self up, making 187 | // the assumption that it, too, will destroy any orphaned objects 188 | // along the way. 189 | entry.value_ptr.deinit(self.alloc); 190 | } 191 | // And now these two lines should remove all remaining metadata the 192 | // HashMap itself stores and leave us with a defunct HashMap. 193 | self.dictionary.clearAndFree(); 194 | self.dictionary.deinit(); 195 | 196 | var symbol_iter = self.symbols.valueIterator(); 197 | while (symbol_iter.next()) |entry| { 198 | _ = entry.decrement_and_prune(.FreeInner, self.alloc); 199 | } 200 | self.symbols.clearAndFree(); 201 | self.symbols.deinit(); 202 | 203 | self.signatures.clearAndFree(); 204 | self.signatures.deinit(); 205 | } 206 | 207 | /// Deinitialize this Runtime, panicking if anything was left on the stack. 208 | /// This can only be used in tests, and will @compileError in non-test 209 | /// builds. This is often the correct function to use in tests, as it 210 | /// forces manual stack cleanup of expected entities, and any garbage left 211 | /// on the stack at that point (bugs) will panic the test. 212 | /// 213 | /// 214 | /// This should be called with `defer` immediately after the Runtime is 215 | /// instantiated (thus the use of `@panic` instead of assertions from 216 | /// `std.testing`, since `defer try` is not valid in Zig). 217 | pub fn deinit_guard_for_empty_stack(self: *Self) void { 218 | // This is also handled in Stack.deinit_guard_for_empty, but the error 219 | // will be more clear originating from the actual function the caller 220 | // used rather than down-stack, since both functions have the same 221 | // usecases. 222 | if (!builtin.is_test) { 223 | @compileError("deinit_guard_for_empty_stack should NEVER be used outside of the test framework"); 224 | } 225 | 226 | // first, ensure the stack is empty and if so, deinitialize it. 227 | self.stack.deinit_guard_for_empty(); 228 | self.deinit_shared(); 229 | } 230 | 231 | /// Release a reference to an Object sent to the heap with 232 | /// `stack_pop_to_heap`, freeing the underlying memory per the rules of 233 | /// `Object.deinit` if no longer used. 234 | pub fn release_heaped_object_reference(self: *Self, ptr: *Object) void { 235 | ptr.deinit(self.alloc); 236 | } 237 | 238 | /// Run a string of input in this Runtime, splitting into words along the 239 | /// way. Any number of WORD_SPLITTING_CHARS are used as delimiters to split 240 | /// the input into potentially-parseable words, which are then passed to 241 | /// `dispatch_word_by_input`. 242 | pub fn eval(self: *Self, input: []const u8) !void { 243 | var current_word: []const u8 = undefined; 244 | var start_idx: usize = 0; 245 | var in_word = false; 246 | var in_string = false; 247 | 248 | chars: for (input) |chr, idx| { 249 | if (in_string and chr != helpers.CHAR_QUOTE_DBL) continue; 250 | 251 | if (chr == helpers.CHAR_QUOTE_DBL) { 252 | if (in_word and !in_string) { 253 | return InternalError.InvalidWordName; 254 | } 255 | 256 | in_string = !in_string; 257 | } 258 | 259 | // TODO: benchmark whether this should be explicitly unrolled or 260 | // just left to the compiler to figure out 261 | inline for (WORD_SPLITTING_CHARS) |candidate| { 262 | if (chr == candidate) { 263 | if (!in_word) continue :chars; 264 | 265 | current_word = input[start_idx..idx]; 266 | try self.dispatch_word_by_input(current_word); 267 | in_word = false; 268 | continue :chars; 269 | } 270 | } 271 | 272 | if (!in_word) { 273 | start_idx = idx; 274 | in_word = true; 275 | } 276 | 277 | if (idx == input.len - 1) { 278 | current_word = input[start_idx..]; 279 | try self.dispatch_word_by_input(current_word); 280 | } 281 | } 282 | } 283 | 284 | /// Pass a single pre-whitespace-trimmed word to ParsedWord.from_input and 285 | /// either place the literal onto the stack or lookup and run the word (if 286 | /// it exists), as appropriate. 287 | pub fn dispatch_word_by_input(self: *Self, input: []const u8) !void { 288 | switch (try ParsedWord.from_input(input)) { 289 | .Simple, .Ref => return InternalError.Unimplemented, 290 | .String => |str| { 291 | const interned_str = try self.get_or_put_string(str); 292 | try self.stack_push_string(interned_str.value_ptr); 293 | }, 294 | .Symbol => |sym| { 295 | const interned_sym = try self.get_or_put_symbol(sym); 296 | try self.stack_push_symbol(interned_sym.value_ptr); 297 | }, 298 | .NumFloat => |num| try self.stack_push_float(num), 299 | .SignedInt => |num| try self.stack_push_sint(num), 300 | .UnsignedInt => |num| try self.stack_push_uint(num), 301 | } 302 | } 303 | 304 | pub fn run_word(self: *Self, word: *Types.HeapedWord) !void { 305 | // TODO: Stack compatibility check against the WordSignature. 306 | 307 | if (word.value) |iword| { 308 | switch (iword.impl) { 309 | .Compound => return InternalError.Unimplemented, // TODO 310 | .HeapLit => |lit| self.stack = try self.stack.do_push(lit.*), 311 | .Primitive => |impl| try impl(self), 312 | } 313 | } else { 314 | // TODO: determine if there's a better/more concise error to pass 315 | // here, perhaps by somehow triggering this and seeing what states 316 | // can even leave us here 317 | return InternalError.EmptyWord; 318 | } 319 | } 320 | 321 | pub fn get_or_put_string(self: *Self, str: []const u8) !GetOrPutResult(Types.HeapedString) { 322 | // TODO: intern this similarly to symbols 323 | const stored = try self.alloc.alloc(u8, str.len); 324 | std.mem.copy(u8, stored[0..], str); 325 | const heaped = try self.alloc.create(Types.HeapedString); 326 | heaped.* = Types.HeapedString.init(stored); 327 | return .{ 328 | .value_ptr = heaped, 329 | .found_existing = false, 330 | }; 331 | } 332 | 333 | /// Retrieve the previously-interned Symbol's Rc 334 | pub fn get_or_put_symbol(self: *Self, sym: []const u8) !GetOrPutResult(Types.HeapedSymbol) { 335 | var entry = try self.symbols.getOrPut(sym); 336 | if (!entry.found_existing) { 337 | const stored = try self.alloc.alloc(u8, sym.len); 338 | std.mem.copy(u8, stored[0..], sym); 339 | entry.value_ptr.* = Types.HeapedSymbol.init(stored); 340 | // TODO: uncomment this to fix known memory bugs found when 341 | // implementing test_protolang 19 Jan 2023 342 | // try entry.value_ptr.increment(); 343 | } 344 | return .{ 345 | .value_ptr = entry.value_ptr, 346 | .found_existing = entry.found_existing, 347 | }; 348 | } 349 | 350 | /// Take a WordSignature by value and, if it is new to this Runtime, store 351 | /// it. Return a GetOrPutResult which will contain a pointer to the stored 352 | /// WordSignature. Each unique signature will be stored a maximum of one 353 | /// time in this Runtime. 354 | /// 355 | /// Can fail by way of allocation errors only. 356 | pub fn get_or_put_word_signature(self: *Self, sig: WordSignature) !GetOrPutResult(WordSignature) { 357 | var entry = try self.signatures.getOrPut(sig); 358 | return .{ 359 | .value_ptr = entry.key_ptr, 360 | .found_existing = entry.found_existing, 361 | }; 362 | } 363 | 364 | pub fn get_well_known_shape(self: *Self, req: WellKnownShape) *Shape { 365 | return &self.well_known_shapes[@enumToInt(req)]; 366 | } 367 | 368 | pub fn get_well_known_word_signature(self: *Self, req: WellKnownSignature) *WordSignature { 369 | return self.well_known_signatures[@enumToInt(req)]; 370 | } 371 | 372 | /// Takes a bare Word struct, wraps it in a refcounter, and returns a 373 | /// pointer to the resultant memory. Does not wrap it in an Object for 374 | /// direct placement on a Stack. 375 | fn send_word_to_heap(self: *Self, bare: Word) !*Types.HeapedWord { 376 | const heap_space = try self.alloc.create(Types.HeapedWord); 377 | heap_space.* = Types.HeapedWord.init(bare); 378 | return heap_space; 379 | } 380 | 381 | /// Heap-wraps a compound word definition. 382 | pub fn word_from_compound_impl( 383 | self: *Self, 384 | impl: CompoundImplementation, 385 | sig: ?Word.SignatureState, 386 | ) !*Types.HeapedWord { 387 | return try self.send_word_to_heap(Word.new_compound_untagged(impl, sig)); 388 | } 389 | 390 | /// Heap-wraps a heaplit word definition. How meta. 391 | pub fn word_from_heaplit_impl( 392 | self: *Self, 393 | impl: HeapLitImplementation, 394 | sig: ?Word.SignatureState, 395 | ) !*Types.HeapedWord { 396 | return try self.send_word_to_heap(Word.new_heaplit_untagged(impl, sig)); 397 | } 398 | 399 | /// Heap-wraps a primitive word definition. 400 | pub fn word_from_primitive_impl( 401 | self: *Self, 402 | impl: PrimitiveImplementation, 403 | sig: ?Word.SignatureState, 404 | ) !*Types.HeapedWord { 405 | return try self.send_word_to_heap(Word.new_primitive_untagged(impl, sig)); 406 | } 407 | 408 | // Right now, Zig doesn't have a way to narrow `targets` type from anytype, 409 | // which is super disappointing, but being brainstormed on: 410 | // https://github.com/ziglang/zig/issues/5404 411 | pub fn define_word_va(self: *Self, identifier: *Types.HeapedSymbol, targets: anytype) !void { 412 | try identifier.increment(); 413 | var dict_entry = try self.dictionary.getOrPut(identifier); 414 | if (!dict_entry.found_existing) { 415 | dict_entry.value_ptr.* = WordList.init(self.alloc); 416 | } 417 | 418 | const compound_storage = try self.alloc.alloc(*Types.HeapedWord, targets.len); 419 | inline for (targets) |target, idx| compound_storage[idx] = target; 420 | 421 | // TODO WARNING: For now, this always makes invalid words (without a 422 | // signature). Need to figure out the correct way to plumb signatures 423 | // here given that the runtime will be using a builder pattern to 424 | // attach them. 425 | var heap_for_word = try self.word_from_compound_impl(compound_storage, null); 426 | 427 | // TODO should this increment actually be stashed away in a dictionary 428 | // helper method somewhere? should there be a 429 | // Runtime.unstacked_word_from_compound_impl that handles the increment 430 | // for us (using Rc.init_referenced) since we can't rely on 431 | // Stack.do_push's implicit increment? 432 | try heap_for_word.increment(); 433 | 434 | try dict_entry.value_ptr.append(heap_for_word); 435 | } 436 | 437 | pub fn priv_space_set_byte(self: *Self, member: u8, value: u8) InternalError!void { 438 | return switch (member) { 439 | 0 => self.private_space.interpreter_mode = @intToEnum(InterpreterMode, value), 440 | else => InternalError.ValueError, 441 | }; 442 | } 443 | 444 | test "priv_space_set_byte" { 445 | var rt = try Self.init(testAllocator); 446 | defer rt.deinit(); 447 | try expectEqual(@as(u8, 0), @enumToInt(rt.private_space.interpreter_mode)); 448 | try rt.priv_space_set_byte(0, 1); 449 | try expectEqual(@as(u8, 1), @enumToInt(rt.private_space.interpreter_mode)); 450 | } 451 | 452 | pub fn stack_peek(self: *Self) !*Object { 453 | return self.stack.do_peek(); 454 | } 455 | 456 | pub fn stack_peek_pair(self: *Self) !Types.PeekPair { 457 | return self.stack.do_peek_pair(); 458 | } 459 | 460 | pub fn stack_peek_trio(self: *Self) !Types.PeekTrio { 461 | return self.stack.do_peek_trio(); 462 | } 463 | 464 | /// Remove the top item from the stack and return it. If there are no 465 | /// contents remaining, a StackManipulationError.Underflow is raised. 466 | pub fn stack_pop(self: *Self) !Object { 467 | const popped = try self.stack.do_pop(); 468 | self.stack = popped.now_top_stack; 469 | return popped.item; 470 | } 471 | 472 | /// Remove the top item from the stack, move it to the heap, and return the 473 | /// new address to the data. Use `release_heaped_object_reference` to later 474 | /// drop a reference to this data (and, if applicable, collect the 475 | /// garbage). If there are no contents remaining, a 476 | /// StackManipulationError.Underflow is raised. 477 | pub fn stack_pop_to_heap(self: *Self) !*Object { 478 | const popped = try self.stack.do_pop(); 479 | const banish_target = try self.alloc.create(Object); 480 | errdefer self.alloc.destroy(banish_target); 481 | banish_target.* = popped.item; 482 | self.stack = popped.now_top_stack; 483 | return banish_target; 484 | } 485 | 486 | /// Remove the top two items from the stack and return them. If there 487 | /// aren't at least two Objects remaining, a 488 | /// StackManipulationError.Underflow is raised. If this happens with one 489 | /// Object on the Stack, it will remain there. 490 | pub fn stack_pop_pair(self: *Self) !Types.PopPairExternal { 491 | const popped = try self.stack.do_pop_pair(); 492 | self.stack = popped.now_top_stack; 493 | return Types.PopPairExternal{ 494 | .near = popped.near, 495 | .far = popped.far, 496 | }; 497 | } 498 | 499 | /// Remove the top three items from the stack and return them. If there 500 | /// aren't at least three Objects remaining, a 501 | /// StackManipulationError.Underflow is raised. If this happens with one or 502 | /// two Objects on the Stack, they will remain there. 503 | pub fn stack_pop_trio(self: *Self) !Types.PopTrioExternal { 504 | const popped = try self.stack.do_pop_trio(); 505 | self.stack = popped.now_top_stack; 506 | return Types.PopTrioExternal{ 507 | .near = popped.near, 508 | .far = popped.far, 509 | .farther = popped.farther, 510 | }; 511 | } 512 | 513 | pub fn stack_push_array(self: *Self, value: *Types.HeapedArray) !void { 514 | self.stack = try self.stack.do_push_array(value); 515 | } 516 | 517 | pub fn stack_push_bool(self: *Self, value: bool) !void { 518 | self.stack = try self.stack.do_push_bool(value); 519 | } 520 | 521 | pub fn stack_push_float(self: *Self, value: f64) !void { 522 | self.stack = try self.stack.do_push_float(value); 523 | } 524 | 525 | pub fn stack_push_sint(self: *Self, value: isize) !void { 526 | self.stack = try self.stack.do_push_sint(value); 527 | } 528 | 529 | /// Push a HeapedString to the stack by reference. As this string is 530 | /// expected to already be heap-allocated and reference-counted, it is also 531 | /// expected that callers have already handled any desired interning before 532 | /// reaching this point. 533 | pub fn stack_push_string(self: *Self, value: *Types.HeapedString) !void { 534 | self.stack = try self.stack.do_push_string(value); 535 | } 536 | 537 | /// Push a HeapedSymbol to the stack by reference. As this symbol is 538 | /// expected to already be heap-allocated and reference-counted, it is also 539 | /// expected that callers have already handled any desired interning before 540 | /// reaching this point. 541 | pub fn stack_push_symbol(self: *Self, value: *Types.HeapedSymbol) !void { 542 | self.stack = try self.stack.do_push_symbol(value); 543 | } 544 | 545 | pub fn stack_push_uint(self: *Self, value: usize) !void { 546 | self.stack = try self.stack.do_push_uint(value); 547 | } 548 | 549 | pub fn stack_push_raw_word(self: *Self, value: *Types.HeapedWord) !void { 550 | self.stack = try self.stack.do_push_word(value); 551 | } 552 | 553 | pub const StackWranglingOperation = enum { 554 | DropTopObject, 555 | 556 | DuplicateTopObject, 557 | DuplicateTopTwoObjectsShuffled, 558 | 559 | SwapTopTwoObjects, 560 | }; 561 | 562 | // TODO: return type? 563 | pub fn stack_wrangle(self: *Self, operation: StackWranglingOperation) !void { 564 | switch (operation) { 565 | .DropTopObject => self.stack = try self.stack.do_drop(), 566 | 567 | .DuplicateTopObject => self.stack = try self.stack.do_dup(), 568 | .DuplicateTopTwoObjectsShuffled => self.stack = try self.stack.do_2dupshuf(), 569 | 570 | .SwapTopTwoObjects => try self.stack.do_swap(), 571 | } 572 | } 573 | }; 574 | 575 | test "Runtime.eval: integration" { 576 | var rt = try Runtime.init(testAllocator); 577 | defer rt.deinit_guard_for_empty_stack(); 578 | 579 | // Push four numbers to the stack individually 580 | try rt.eval("-1"); 581 | try rt.eval("+2"); 582 | try rt.eval("3.14"); 583 | try rt.eval("4"); 584 | 585 | // Push a symbol for giggles 586 | try rt.eval(":something"); 587 | 588 | // Push a string too 589 | try rt.eval("\"foo and a bit of bar\""); 590 | 591 | // Now push several more numbers in one library call 592 | try rt.eval("5 +6 7.5"); 593 | 594 | var float_signed_unsigned = try rt.stack_pop_trio(); 595 | defer { 596 | rt.release_heaped_object_reference(&float_signed_unsigned.near); 597 | rt.release_heaped_object_reference(&float_signed_unsigned.far); 598 | rt.release_heaped_object_reference(&float_signed_unsigned.farther); 599 | } 600 | try expectApproxEqAbs( 601 | @as(f64, 7.5), 602 | float_signed_unsigned.near.Float, 603 | @as(f64, 0.0000001), 604 | ); 605 | try expectEqual(@as(isize, 6), float_signed_unsigned.far.SignedInt); 606 | try expectEqual(@as(usize, 5), float_signed_unsigned.farther.UnsignedInt); 607 | 608 | var foo_str = try rt.stack_pop(); 609 | defer { 610 | rt.release_heaped_object_reference(&foo_str); 611 | } 612 | try expectEqualStrings("foo and a bit of bar", foo_str.String.value.?); 613 | 614 | var something_symbol = try rt.stack_pop(); 615 | defer { 616 | // TODO: uncomment this once Runtime.get_or_put_symbol is fixed to 617 | // increment refcount correctly, this *should* be leaking RAM as-is but 618 | // is not, unearthing a whole class of bugs (5 addresses leaking in 1 619 | // test in libgale alone) 620 | // 621 | // rt.release_heaped_object_reference(&something_symbol); 622 | } 623 | try expectEqualStrings("something", something_symbol.Symbol.value.?); 624 | 625 | var inferunsigned_float_signed = try rt.stack_pop_trio(); 626 | defer { 627 | rt.release_heaped_object_reference(&inferunsigned_float_signed.near); 628 | rt.release_heaped_object_reference(&inferunsigned_float_signed.far); 629 | rt.release_heaped_object_reference(&inferunsigned_float_signed.farther); 630 | } 631 | try expectEqual(@as(usize, 4), inferunsigned_float_signed.near.UnsignedInt); 632 | try expectApproxEqAbs( 633 | @as(f64, 3.14), 634 | inferunsigned_float_signed.far.Float, 635 | @as(f64, 0.0000001), 636 | ); 637 | try expectEqual(@as(isize, 2), inferunsigned_float_signed.farther.SignedInt); 638 | 639 | var bottom = try rt.stack_pop(); 640 | defer rt.release_heaped_object_reference(&bottom); 641 | try expectEqual(@as(isize, -1), bottom.SignedInt); 642 | } 643 | 644 | test { 645 | std.testing.refAllDecls(@This()); 646 | } 647 | -------------------------------------------------------------------------------- /lib/gale/shape.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const Allocator = std.mem.Allocator; 16 | const testAllocator: Allocator = std.testing.allocator; 17 | const expect = std.testing.expect; 18 | const expectEqual = std.testing.expectEqual; 19 | const expectEqualStrings = std.testing.expectEqualStrings; 20 | const expectError = std.testing.expectError; 21 | 22 | const InternalError = @import("./internal_error.zig").InternalError; 23 | const Types = @import("./types.zig"); 24 | const WordSignature = @import("./word_signature.zig"); 25 | 26 | // TODO: Configurable in build.zig 27 | const NUM_INLINED_SHAPES_IN_GENERICS: usize = 2; 28 | 29 | // TODO: Should this ever be localized or configurable in build.zig? 30 | const ANONYMOUS_SHAPE_FILLER_NAME = ""; 31 | 32 | pub const CATCHALL_HOLDING_TYPE = u8; 33 | 34 | // TODO: move to a common file somewhere 35 | const AtomicUsize = std.atomic.Atomic(usize); 36 | 37 | // TODO: Many of the enums contained within this struct can be sized down to 38 | // u1/u2/u4s, but only with Zig 0.11+ due to compiler crashes as described in 39 | // https://github.com/ziglang/zig/issues/13812 (resolved Dec 2022). 40 | pub const Shape = struct { 41 | const Self = @This(); 42 | 43 | given_name: ?*Types.HeapedSymbol = null, 44 | receiver_words: ?[]MemberWord = null, 45 | contents: ShapeContents, 46 | 47 | /// Shape Evolution is how we implement the "newtype" concept: say I have 48 | /// a method which can only take a FibonnaciNumber. All fibonnaci numbers 49 | /// can be represented within the space of unsigned integers, but not all 50 | /// unsigned integers are valid fibonnaci numbers. In many languages, the 51 | /// type-safe way to represent this is to wrap ("box") the integer in some 52 | /// sort of class/dictionary/struct, perhaps with a property name of .value 53 | /// or (in Rust's case) something like .0. In Gale, like Haskell and other 54 | /// languages, we can instead create a shape that is *exactly* and *only* 55 | /// an unsigned integer under the hood, and is disparate from all other 56 | /// unsigned integers (or other types composed from them: perhaps Meters). 57 | /// We call this concept "evolving" a shape, to disambiguate it from 58 | /// various other concepts that I might otherwise borrow the names of 59 | /// for this purpose (say, "aliasing", which is *also* something you can 60 | /// do to a shape, but this has nothing to do with our "newtype" concept). 61 | /// 62 | /// The evolved shape will retain `receiver_words` (making this a reasonably 63 | /// lightweight way to non-destructively extend or override an existing 64 | /// Shape's contract requirements), and all "Self Words" defined on the 65 | /// root will be "inherited" to this evolved shape (with Self redefined). 66 | /// Take care with evolving bounded shapes, because `in-bounds?` will be 67 | /// inherited unless explicitly overridden. 68 | /// 69 | /// Yes, dear reader, this is, other than a memory simplicity trick, 70 | /// an approximation of prototypical inheritance, and generally a path 71 | /// towards loose object orientation. 72 | evolved_from: ?*Self = null, 73 | evolution_id: usize = 0, 74 | evolutions_spawned: AtomicUsize = AtomicUsize.init(0), 75 | 76 | pub const Boundedness = enum(u1) { 77 | Bounded, 78 | Unbounded, 79 | }; 80 | 81 | // These MUST be kept in sync with the Bounded/Unbounded enums in 82 | // ShapeContents below! This is enforced with the comptime block. 83 | pub const Primitives = enum(u4) { 84 | Array, 85 | Boolean, 86 | CharSlice, 87 | Float, 88 | SignedInt, 89 | UnsignedInt, 90 | Word, 91 | WordSignature, 92 | 93 | comptime { 94 | std.debug.assert(@enumToInt(Primitives.Array) == @enumToInt(ShapeContents.BoundedPrimitive.Array)); 95 | std.debug.assert(@enumToInt(Primitives.Boolean) == @enumToInt(ShapeContents.BoundedPrimitive.Boolean)); 96 | std.debug.assert(@enumToInt(Primitives.CharSlice) == @enumToInt(ShapeContents.BoundedPrimitive.CharSlice)); 97 | std.debug.assert(@enumToInt(Primitives.Float) == @enumToInt(ShapeContents.BoundedPrimitive.Float)); 98 | std.debug.assert(@enumToInt(Primitives.SignedInt) == @enumToInt(ShapeContents.BoundedPrimitive.SignedInt)); 99 | std.debug.assert(@enumToInt(Primitives.UnsignedInt) == @enumToInt(ShapeContents.BoundedPrimitive.UnsignedInt)); 100 | std.debug.assert(@enumToInt(Primitives.Word) == @enumToInt(ShapeContents.BoundedPrimitive.Word)); 101 | std.debug.assert(@enumToInt(Primitives.WordSignature) == @enumToInt(ShapeContents.BoundedPrimitive.WordSignature)); 102 | } 103 | }; 104 | 105 | /// Convenience wrapper around creating a Shape with Contents of a 106 | /// CatchAll variety, a process which otherwise takes several lines 107 | /// of tagged union instantiation. 108 | pub fn new_containing_catchall(value: CATCHALL_HOLDING_TYPE) Self { 109 | return Self{ .contents = ShapeContents{ .CatchAll = value } }; 110 | } 111 | 112 | /// Convenience wrapper around creating a Shape with Contents of a 113 | /// primitive variety, a process which otherwise takes several lines of 114 | /// tagged union instantiation. 115 | pub fn new_containing_primitive(boundedness: Boundedness, primitive: Primitives) Self { 116 | return switch (boundedness) { 117 | .Bounded => bounded: { 118 | const cast_primitive = @intToEnum(ShapeContents.BoundedPrimitive, @enumToInt(primitive)); 119 | const primitive_contents = ShapeContents.PrimitiveContents{ .Bounded = cast_primitive }; 120 | const contents = ShapeContents{ .Primitive = primitive_contents }; 121 | break :bounded Self{ .contents = contents }; 122 | }, 123 | .Unbounded => unbounded: { 124 | const cast_primitive = @intToEnum(ShapeContents.UnboundedPrimitive, @enumToInt(primitive)); 125 | const primitive_contents = ShapeContents.PrimitiveContents{ .Unbounded = cast_primitive }; 126 | const contents = ShapeContents{ .Primitive = primitive_contents }; 127 | break :unbounded Self{ .contents = contents }; 128 | }, 129 | }; 130 | } 131 | 132 | /// Evolve this Shape into an independent Shape laid out identically in 133 | /// memory which will not fulfill Word Signatures expecting the root 134 | /// Shape (and vice-versa). This is analogous to the "newtype" paradigm 135 | /// in other languages; see Shape.evolved_from docstring for more details 136 | /// and particulars about what the evolved Shape will look like or be able 137 | /// to do. 138 | /// 139 | /// The evolved shape will not have a `given_name`, but will have a pointer 140 | /// to the parent in `evolved_from` which may have a `given_name` from which 141 | /// to derive a new `given_name`, if desired. 142 | pub fn evolve(self: *Self) Self { 143 | const evolution_id = self.evolutions_spawned.fetchAdd(1, .Monotonic); 144 | 145 | return Self{ 146 | .given_name = null, 147 | .receiver_words = self.receiver_words, 148 | .contents = self.contents, 149 | .evolved_from = self, 150 | .evolution_id = evolution_id, 151 | .evolutions_spawned = AtomicUsize.init(0), 152 | }; 153 | } 154 | 155 | /// Returns the given name of the shape or :anonymous if no name is known. 156 | /// Accepts an allocator which is used to create the space for the 157 | /// :anonymous symbol if needed. As usual for any type implemented with 158 | /// Rc(_), the return value of this method must be decremented and 159 | /// eventually pruned to avoid memory leaks. 160 | /// 161 | /// This method can fail in any ways an allocation can fail, and further, 162 | /// will panic if the underlying Rc of a symbol it attempts to increment 163 | /// refcounts of is corrupt. 164 | // 165 | // TODO: This shouldn't take an allocator directly, but should take a 166 | // symbol pool from which we can request an existing :anonymous if it 167 | // already exists (perhaps we should ensure that it always will). The 168 | // current implementation wastes tons of RAM in the event of calling name() 169 | // many times on unnamed shapes. This basically entails decoupling a 170 | // conceptual SymbolPool from the Runtime where it currently exists, 171 | // because I blatantly refuse to initialize an entire Runtime{} here 172 | // just to use its symbol pool... at least for as long as I can get away 173 | // with it. 174 | pub fn name(self: *Self, alloc: Allocator) !*Types.HeapedSymbol { 175 | var given_name_symbol: *Types.HeapedSymbol = undefined; 176 | 177 | if (self.given_name) |gname| { 178 | given_name_symbol = gname; 179 | } else { 180 | const symbol_space = try alloc.alloc(u8, ANONYMOUS_SHAPE_FILLER_NAME.len); 181 | std.mem.copy(u8, symbol_space, ANONYMOUS_SHAPE_FILLER_NAME); 182 | given_name_symbol = try alloc.create(Types.HeapedSymbol); 183 | // TODO: determine if we consider this to be a "gale-side reference" 184 | // as per the docs of Rc.init_referenced. If not, it may make sense 185 | // to update the docstring there, because we'll have made it a lie... 186 | given_name_symbol.* = Types.HeapedSymbol.init(symbol_space); 187 | } 188 | 189 | if (given_name_symbol.increment()) |_| { 190 | return given_name_symbol; 191 | } else |err| { 192 | switch (err) { 193 | InternalError.AttemptedResurrectionOfExhaustedRc => @panic("shape name's symbol's underlying Rc has been exhausted, this is a memory management bug in Gale"), 194 | else => @panic("failed to increment shape name's underlying Rc"), 195 | } 196 | } 197 | } 198 | 199 | test "anonymous shapes have a filler name" { 200 | var shape = Self{ .contents = .Empty }; 201 | const shape_name = try shape.name(testAllocator); 202 | defer { 203 | _ = shape_name.decrement_and_prune(.FreeInnerDestroySelf, testAllocator); 204 | } 205 | try expectEqualStrings(ANONYMOUS_SHAPE_FILLER_NAME, shape_name.value.?); 206 | } 207 | 208 | test "requesting the name of a shape qualifies as owning a ref to the underlying symbol" { 209 | const given_name = "AnotherEternity"; 210 | const symbol_space = try testAllocator.alloc(u8, given_name.len); 211 | std.mem.copy(u8, symbol_space, given_name); 212 | const name_symbol = try testAllocator.create(Types.HeapedSymbol); 213 | name_symbol.* = Types.HeapedSymbol.init_referenced(symbol_space); 214 | defer { 215 | _ = name_symbol.decrement_and_prune(.FreeInnerDestroySelf, testAllocator); 216 | } 217 | 218 | var shape = Self{ .contents = .Empty }; 219 | shape.given_name = name_symbol; 220 | 221 | const shape_name = try shape.name(testAllocator); 222 | defer { 223 | _ = shape_name.decrement_and_prune(.FreeInnerDestroySelf, testAllocator); 224 | } 225 | try expectEqual(shape_name.strong_count.value, 2); 226 | } 227 | 228 | pub const ShapeIncompatibilityReason = enum(u4) { 229 | Incomparable, 230 | DisparateEvolutionBases, 231 | DisparateEvolutions, 232 | DisparateUnderlyingPrimitives, 233 | // Not used in this file, but synthesized by WordSignature.detect_incompatibilities 234 | CatchAllMultipleResolutionCandidates, 235 | }; 236 | 237 | pub const ShapeCompatibilityResult = union(enum) { 238 | Compatible, 239 | Incompatible: ShapeIncompatibilityReason, 240 | Indeterminate, 241 | }; 242 | 243 | const SCR = ShapeCompatibilityResult; 244 | 245 | /// Determine, if possible statically, whether two shapes are compatible, 246 | /// using `self` as the reference (in other words: "is `other` able to 247 | /// fulfill my constraints?"). Null values are indeterminate statically 248 | /// and generally speaking need to fall back to runtime determination via 249 | /// BoundsCheckable (using in-bounds?), or at least require further context 250 | /// not knowable at the Shape level (eg. for CatchAll shapes, which require 251 | /// knowledge of an entire WordSignature to make sense). 252 | pub fn compatible_with(self: *Self, other: *Self) SCR { 253 | return switch (self.contents) { 254 | .Empty => self.detect_incomparability(other) orelse 255 | self.detect_evolutionary_incompatibility(other) orelse 256 | SCR.Compatible, 257 | .CatchAll => self.catchalls_compatible(other), 258 | .Primitive => self.detect_incomparability(other) orelse 259 | self.detect_evolutionary_incompatibility(other) orelse 260 | self.primitives_compatible(other), 261 | }; 262 | } 263 | 264 | inline fn detect_evolutionary_incompatibility(self: *Self, other: *Self) ?SCR { 265 | if (!std.meta.eql(self.evolved_from, other.evolved_from)) return SCR{ .Incompatible = .DisparateEvolutionBases }; 266 | if (self.evolution_id != other.evolution_id) return SCR{ .Incompatible = .DisparateEvolutions }; 267 | 268 | return null; 269 | } 270 | 271 | inline fn detect_incomparability(self: *Self, other: *Self) ?SCR { 272 | const self_kind = std.meta.activeTag(self.contents); 273 | const other_kind = std.meta.activeTag(other.contents); 274 | if (self_kind != other_kind) return SCR{ .Incompatible = .Incomparable }; 275 | return null; 276 | } 277 | 278 | inline fn catchalls_compatible(self: *Self, other: *Self) SCR { 279 | const self_val = self.contents.CatchAll; 280 | return switch (other.contents) { 281 | // @1 == @1, but @1 and @2 are not necessarily incompatible: 282 | // consider ( @1 -> @1 ) and ( @2 -> @2 ). These are identical 283 | // logically, despite being written differently. Thus, we can 284 | // never statically know that two CatchAlls are *in*compatible, 285 | // only that they are or *might* be. The rest must be figured 286 | // out by the word signature checker, which has the full 287 | // context to know whether @1 could be @2. 288 | // 289 | // TODO: Does this actually mean that CatchAlls aren't Shapes 290 | // at all? They already are an extreme misfit within the enum, 291 | // maybe what's currently a CatchAll is actually an Evolved 292 | // form of an UnsignedInt which should be captured and mangled 293 | // as necessary by the WordSignature checker, never reaching 294 | // this altitude. 295 | .CatchAll => |other_val| if (self_val == other_val) SCR.Compatible else SCR.Indeterminate, 296 | // ( @1 -> @1 ) and ( Boolean -> Boolean ) are compatible, but 297 | // there's no possible way to know that at this altitude where 298 | // we're comparing just one shape from each signature. Punt 299 | // this entire decision process up a level in the call tree. 300 | else => SCR.Indeterminate, 301 | }; 302 | } 303 | 304 | inline fn primitives_compatible(self: *Self, other: *Self) SCR { 305 | const self_val = self.contents.Primitive; 306 | const other_val = other.contents.Primitive; 307 | 308 | return switch (self_val) { 309 | .Bounded => |sval| bounded: { 310 | const comparator = switch (other_val) { 311 | .Bounded => @enumToInt(other_val.Bounded), 312 | .Unbounded => @enumToInt(other_val.Unbounded), 313 | }; 314 | 315 | if (comparator == @enumToInt(sval)) break :bounded SCR.Indeterminate; 316 | 317 | break :bounded SCR{ .Incompatible = .DisparateUnderlyingPrimitives }; 318 | }, 319 | .Unbounded => |sval| unbounded: { 320 | const compatible = switch (other_val) { 321 | .Unbounded => |oval| sval == oval, 322 | // The usecase for self being unbounded but other being 323 | // bounded is yet-unknown but the code is fairly trivial 324 | // to write so we'll support it... for now? 325 | .Bounded => |oval| @enumToInt(sval) == @enumToInt(oval), 326 | }; 327 | 328 | if (compatible) break :unbounded SCR.Compatible; 329 | 330 | break :unbounded SCR{ .Incompatible = .DisparateUnderlyingPrimitives }; 331 | }, 332 | }; 333 | } 334 | }; 335 | 336 | pub const MemberWord = struct { 337 | given_name: *Types.HeapedSymbol, 338 | signature: WordSignature, 339 | }; 340 | 341 | pub const ShapeContents = union(enum) { 342 | const PrimitiveContents = union(enum) { 343 | /// These are the most general cases: *any* boolean value, *any* 344 | /// string, *any* uint, etc. 345 | Unbounded: UnboundedPrimitive, 346 | 347 | /// These are special cases that aren't yet implemented (TODO: update 348 | /// this comment when they are...): a word can specify that it accepts 349 | /// exactly the integer "2", or perhaps only the string "foo". These 350 | /// word signatures are then represented with a non-evolved clone of 351 | /// the shape, with the Unbounded enum member converted to a Bounded 352 | /// member, which will trigger the slower validity checks during type 353 | /// checking. This concept has a fancy name in type system theory, but 354 | /// I'm offline right now and can't look it up. This entire type system 355 | /// is being written by the seat of my pants, I'm not an academic :) 356 | /// 357 | /// Bounded shapes *must* fulfill what is at runtime known as the 358 | /// BoundsCheckable shape, which includes an in-bounds? word with 359 | /// signature ( {Unbounded Analogue Shape} <- Boolean ) 360 | Bounded: BoundedPrimitive, 361 | 362 | // Why, you might ask, did I just represent 10 states as 2x5 enums rather 363 | // than a 1x10? Because it makes checking just a *bit* easier to read 364 | // later: rather than checking if something is a BoundedA || BoundedB || 365 | // etc, I can check which family they belong to first, and then match 366 | // the inner "type". 367 | }; 368 | 369 | const BoundedPrimitive = enum(u4) { 370 | Array, 371 | Boolean, 372 | CharSlice, 373 | Float, 374 | SignedInt, 375 | UnsignedInt, 376 | Word, 377 | WordSignature, 378 | }; 379 | 380 | const UnboundedPrimitive = enum(u4) { 381 | Array, 382 | Boolean, 383 | CharSlice, 384 | Float, 385 | SignedInt, 386 | UnsignedInt, 387 | Word, 388 | WordSignature, 389 | }; 390 | 391 | comptime { 392 | // Since we depend on these integer values matching as part of 393 | // `Shape.compatible_with`, let's paranoically ensure we're not 394 | // going to trigger some unsafe, undefined (or incorrectly-defined) 395 | // behavior later... 396 | std.debug.assert(@enumToInt(UnboundedPrimitive.Array) == @enumToInt(BoundedPrimitive.Array)); 397 | std.debug.assert(@enumToInt(UnboundedPrimitive.Boolean) == @enumToInt(BoundedPrimitive.Boolean)); 398 | std.debug.assert(@enumToInt(UnboundedPrimitive.CharSlice) == @enumToInt(BoundedPrimitive.CharSlice)); 399 | std.debug.assert(@enumToInt(UnboundedPrimitive.Float) == @enumToInt(BoundedPrimitive.Float)); 400 | std.debug.assert(@enumToInt(UnboundedPrimitive.SignedInt) == @enumToInt(BoundedPrimitive.SignedInt)); 401 | std.debug.assert(@enumToInt(UnboundedPrimitive.UnsignedInt) == @enumToInt(BoundedPrimitive.UnsignedInt)); 402 | std.debug.assert(@enumToInt(UnboundedPrimitive.Word) == @enumToInt(BoundedPrimitive.Word)); 403 | std.debug.assert(@enumToInt(UnboundedPrimitive.WordSignature) == @enumToInt(BoundedPrimitive.WordSignature)); 404 | } 405 | 406 | Empty, 407 | /// Shapes are purely metadata for primitive root types: the underlying 408 | /// value isn't "boxed" into a shape struct, instead, Objects with a 409 | /// null shape pointer are assumed to be the respective root shape 410 | /// for the underlying primitive type in memory. In other words, a 411 | /// pair of {null, 8u} is known to be an UnsignedInt type on the Gale 412 | /// side, and only one UnsignedInt shape struct will ever exist in 413 | /// memory (cached in the Runtime) 414 | Primitive: PrimitiveContents, 415 | 416 | /// A CatchAll Shape is used only in Trusted Words' signatures, as it 417 | /// indicates an acceptance of any input. Such a Shape has no other 418 | /// purpose in the language, and will almost certainly Not Do What You 419 | /// Want It To in any other context. 420 | /// 421 | /// These are limited to 256 somewhat arbitrarily, but like, for the love 422 | /// of all that is good in the world, why would you possibly need more than 423 | /// 256 of these? 424 | CatchAll: CATCHALL_HOLDING_TYPE, 425 | }; 426 | 427 | pub const GenericWithinStruct = union(enum) { 428 | /// Small generics are inlined for quicker lookups. 429 | /// 430 | /// Null pointers in .shapes are ambiguous (thus the addition of .slots): 431 | /// 432 | /// - if idx < .slots, null pointers reflect unfilled slots. It's expected 433 | /// (though not yet enforced elsewhere) that the number of unfilled slots 434 | /// will always be either 0, or .slots, never anything in between. In other 435 | /// words, MyShape<_, _> is valid (albeit useless for anything except 436 | /// pattern matching any MyShape), MyShape is valid 437 | /// (and is thus the fully-populated and instantiable form of MyShape), 438 | /// but MyShape is not (yet? TODO determine if this decision 439 | /// should change). 440 | /// 441 | /// - if idx >= .slots, null pointers are unused memory and are out of 442 | /// bounds 443 | SizeBounded: struct { 444 | slots: usize, 445 | shapes: [NUM_INLINED_SHAPES_IN_GENERICS]?*Shape, 446 | }, 447 | /// Larger generics (since the maximum number of member types within a 448 | /// generic is unbounded) have to be heap-allocated in their own space. 449 | SizeUnbounded: []?*Shape, 450 | }; 451 | 452 | test { 453 | std.testing.refAllDeclsRecursive(@This()); 454 | } 455 | -------------------------------------------------------------------------------- /lib/gale/stack.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const assert = std.debug.assert; 16 | const Allocator = std.mem.Allocator; 17 | const testAllocator: Allocator = std.testing.allocator; 18 | const expect = std.testing.expect; 19 | const expectEqual = std.testing.expectEqual; 20 | const expectError = std.testing.expectError; 21 | 22 | const builtin = @import("builtin"); 23 | 24 | const Object = @import("./object.zig").Object; 25 | const Types = @import("./types.zig"); 26 | 27 | pub const StackManipulationError = error{ 28 | Underflow, 29 | Overflow, 30 | RefuseToGrowMultipleStacks, 31 | YouAlmostCertainlyDidNotMeanToUseThisNonTerminalStack, 32 | }; 33 | 34 | /// A doubly-linked list of doubly-linked lists: we'll pretend to have infinite 35 | /// stack space by simply making as many heap-based stacks as we have space 36 | /// for, and seamlessly (as far as the end-user is concerned) glue them 37 | /// together. This should serve to make common gale stack operations reasonably 38 | /// performant with reasonable tradeoffs, as we're moving our way around a 39 | /// block of mass-allocated memory, rather than constantly churning garbage via 40 | /// alloc() and free() calls for each Object (imagining a fully-pointer-based 41 | /// doubly linked list implementation of a "stack", for example std.TailQueue). 42 | /// I suspect it should be uncommon to get anywhere near the end of even a 43 | /// single stack, but if it does happen, we don't have to take the perf hit of 44 | /// copying the old stack to a new stack of size N*2, as one would generally do 45 | /// when resizing lists in most dynamic languages. Whether this complexity pays 46 | /// itself off at any point is somewhat TBD... 47 | /// 48 | /// Operations right at the edge of a stack's space bleeding into the next 49 | /// stack are unlikely to be performant, particularly those that juggle that 50 | /// boundary repeatedly (imagine swapping the top element of a "lower" stack 51 | /// and the bottom element of a "higher" stack in a tight loop or some such). I 52 | /// don't yet have benchmarking data to prove this hypothesis, however. 53 | /// 54 | /// Not thread-safe. Use channels or something. 55 | // TODO: 56 | // - Docs 57 | // - Stop leaking literally everything: keep no more than N+1 stacks allocated 58 | // at a time, once self->next->next->next_idx == 0, it's time to start calling 59 | // free. 60 | pub const Stack = struct { 61 | // Those finding they need more per-stack space should compile their own 62 | // project-specific gale build changing the constant as appropriate. Unlike 63 | // many languages where mucking about with the internals is faux-pas, in 64 | // gale it is encouraged on a "if you know you really need it" basis. 65 | // 66 | // TODO: configurable in build.zig 67 | const STACK_SIZE: usize = 2048; 68 | 69 | comptime { 70 | assert(STACK_SIZE >= 1); 71 | 72 | // If STACK_SIZE for some godforsaken reason is MAX_INT of a usize, 73 | // we'll overflow .next_idx upon assigning the final item in this 74 | // stack. On a 16-bit microcontroller this is vaguely conceivable 75 | // (given that STACK_SIZE would only be ~8x bigger than default), on 76 | // any larger bitsizes this is a near-laughable concept in practical 77 | // usecases, but failure to handle this is an almost-guaranteed CVE 78 | // waiting to happen. 79 | var overflow_space: usize = undefined; 80 | assert(!@addWithOverflow(usize, STACK_SIZE, 1, &overflow_space)); 81 | } 82 | 83 | const Self = @This(); 84 | 85 | alloc: Allocator, 86 | prev: ?*Stack, 87 | next: ?*Stack, 88 | next_idx: usize, 89 | contents: [STACK_SIZE]?Object, 90 | 91 | pub fn init(alloc: Allocator, prev: ?*Stack) !*Self { 92 | var stack = try alloc.create(Self); 93 | stack.* = .{ 94 | .alloc = alloc, 95 | .prev = prev, 96 | .next = null, 97 | .next_idx = 0, 98 | .contents = .{null} ** STACK_SIZE, 99 | }; 100 | return stack; 101 | } 102 | 103 | pub fn deinit(self: *Self) void { 104 | if (self.next) |next| next.deinit(); 105 | 106 | if (self.prev) |prev| prev.next = null; 107 | 108 | while (self.next_idx > 0) { 109 | _ = self.do_drop() catch null; 110 | } 111 | 112 | self.alloc.destroy(self); 113 | } 114 | 115 | // TODO: docs about stack jumping behavior here 116 | // TODO: see if this should just be the mainline deinit() function instead 117 | // or if they can otherwise be merged 118 | pub fn deinit_from_bottom(self: *Self) void { 119 | if (self.prev) |prev| { 120 | prev.deinit_from_bottom(); 121 | } else { 122 | self.deinit(); 123 | } 124 | } 125 | 126 | /// Deinitialize this stack, but only if it is empty. This is a helper 127 | /// function for tests only (all other attempts to use this will result in 128 | /// a @compileError) to ensure no unexpected garbage is left behind on the 129 | /// stack after the test. In tests, this is almost certainly the correct 130 | /// function to use, except when testing garbage collection itself, and 131 | /// should be called with `defer` immediately after the Stack is 132 | /// instantiated (thus the use of `@panic` instead of assertions from 133 | /// `std.testing`, since `defer try` is not valid in Zig). 134 | /// 135 | /// For non-test deinitialization, see `deinit`. 136 | pub fn deinit_guard_for_empty(self: *Self) void { 137 | if (!builtin.is_test) { 138 | @compileError("deinit_guard_for_empty should NEVER be used outside of the test framework"); 139 | } 140 | 141 | if (self.do_peek_pair()) |_| {} else |err| { 142 | if (err != StackManipulationError.Underflow) { 143 | std.debug.panic("do_peek_pair returned non-Underflow error: {any}", .{err}); 144 | } 145 | 146 | return self.deinit(); 147 | } 148 | 149 | std.debug.panic("stack was not empty at deinit time", .{}); 150 | } 151 | 152 | /// Ensures there will be enough space in no more than two stacks to store 153 | /// `count` new objects. Returns pointer to newly created stack if it was 154 | /// necessary to store all items. If no growth was necessary, returns null. 155 | /// The ergonomics of this API make a bit more sense in context: 156 | /// 157 | /// ``` 158 | /// const target = self.expand_to_fit(1) orelse self; 159 | /// 160 | /// // or... 161 | /// 162 | /// if (self.expand_to_fit(5)) |final_destination| { 163 | /// // more complicated logic here to handle stack crossover 164 | /// } else { 165 | /// // happy path here, all on one stack 166 | /// } 167 | /// ``` 168 | fn expand_to_fit(self: *Self, count: usize) !?*Self { 169 | if (self.next_idx + count > STACK_SIZE * 2) { 170 | return StackManipulationError.RefuseToGrowMultipleStacks; 171 | } 172 | 173 | if (self.next_idx + count > STACK_SIZE) { 174 | return try self.onwards(); 175 | } 176 | 177 | return null; 178 | } 179 | 180 | test "expand_to_fit" { 181 | const baseStack = try Self.init(testAllocator, null); 182 | defer baseStack.deinit_guard_for_empty(); 183 | 184 | try expectError( 185 | StackManipulationError.RefuseToGrowMultipleStacks, 186 | baseStack.expand_to_fit(Stack.STACK_SIZE * 2 + 1), 187 | ); 188 | try expectEqual(@as(?*Self, null), try baseStack.expand_to_fit(STACK_SIZE / 2 + 1)); 189 | const new_stack = try baseStack.expand_to_fit(STACK_SIZE * 2); 190 | try expect(new_stack != null); 191 | try expectError( 192 | StackManipulationError.RefuseToGrowMultipleStacks, 193 | new_stack.?.expand_to_fit(Stack.STACK_SIZE * 2 + 1), 194 | ); 195 | new_stack.?.deinit(); 196 | } 197 | 198 | /// Extend the Stack into a new stack, presumably because we've run out of 199 | /// room in the current one (if there is one). Return a pointer to the new 200 | /// stack, as that stack is what callers should now be working with. 201 | fn onwards(self: *Self) !*Self { 202 | var next = try Self.init(self.alloc, self); 203 | self.next = next; 204 | return next; 205 | } 206 | 207 | inline fn non_terminal_stack_guard(self: *Self) StackManipulationError!void { 208 | if (self.next != null) { 209 | return StackManipulationError.YouAlmostCertainlyDidNotMeanToUseThisNonTerminalStack; 210 | } 211 | } 212 | 213 | pub fn do_peek_trio(self: *Self) !Types.PeekTrio { 214 | try self.non_terminal_stack_guard(); 215 | return try @call( 216 | .{ .modifier = .always_inline }, 217 | self.do_peek_trio_no_really_even_on_inner_stacks, 218 | .{}, 219 | ); 220 | } 221 | 222 | pub fn do_peek_trio_no_really_even_on_inner_stacks(self: *Self) !Types.PeekTrio { 223 | if (self.next_idx == 0) { 224 | if (self.prev) |prev| { 225 | return prev.do_peek_trio_no_really_even_on_inner_stacks(); 226 | } 227 | 228 | return StackManipulationError.Underflow; 229 | } 230 | 231 | if (self.next_idx == 1) { 232 | return Types.PeekTrio{ 233 | .near = &self.contents[0].?, 234 | .far = if (self.prev) |prev| 235 | &prev.contents[prev.next_idx - 1].? 236 | else 237 | null, 238 | .farther = if (self.prev) |prev| 239 | &prev.contents[prev.next_idx - 2].? 240 | else 241 | null, 242 | }; 243 | } 244 | 245 | if (self.next_idx == 2) { 246 | return Types.PeekTrio{ 247 | .near = &self.contents[1].?, 248 | .far = &self.contents[0].?, 249 | .farther = if (self.prev) |prev| 250 | &prev.contents[prev.next_idx - 1].? 251 | else 252 | null, 253 | }; 254 | } 255 | 256 | return Types.PeekTrio{ 257 | .near = &self.contents[self.next_idx - 1].?, 258 | .far = &self.contents[self.next_idx - 2].?, 259 | .farther = &self.contents[self.next_idx - 3].?, 260 | }; 261 | } 262 | 263 | test "do_peek_trio" { 264 | const stack = try Self.init(testAllocator, null); 265 | defer stack.deinit_guard_for_empty(); 266 | 267 | try expectError(StackManipulationError.Underflow, stack.do_peek_trio()); 268 | 269 | var target = try stack.do_push_uint(1); 270 | const near_one = try target.do_peek_trio(); 271 | try expectEqual(@as(usize, 1), near_one.near.*.UnsignedInt); 272 | try expectEqual(@as(?*Object, null), near_one.far); 273 | try expectEqual(@as(?*Object, null), near_one.farther); 274 | 275 | target = try target.do_push_uint(2); 276 | const near_two = try target.do_peek_trio(); 277 | try expectEqual(@as(usize, 2), near_two.near.*.UnsignedInt); 278 | try expectEqual(@as(usize, 1), near_two.far.?.*.UnsignedInt); 279 | try expectEqual(@as(?*Object, null), near_one.farther); 280 | 281 | target = try target.do_push_uint(3); 282 | const near_three = try target.do_peek_trio(); 283 | try expectEqual(@as(usize, 3), near_three.near.*.UnsignedInt); 284 | try expectEqual(@as(usize, 2), near_three.far.?.*.UnsignedInt); 285 | try expectEqual(@as(usize, 1), near_three.farther.?.*.UnsignedInt); 286 | 287 | target = try target.do_drop(); 288 | target = try target.do_drop(); 289 | target = try target.do_drop(); 290 | } 291 | 292 | pub fn do_peek_pair(self: *Self) !Types.PeekPair { 293 | try self.non_terminal_stack_guard(); 294 | return try @call( 295 | .{ .modifier = .always_inline }, 296 | self.do_peek_pair_no_really_even_on_inner_stacks, 297 | .{}, 298 | ); 299 | } 300 | 301 | pub fn do_peek_pair_no_really_even_on_inner_stacks(self: *Self) !Types.PeekPair { 302 | if (self.next_idx == 0) { 303 | if (self.prev) |prev| { 304 | return prev.do_peek_pair_no_really_even_on_inner_stacks(); 305 | } 306 | 307 | return StackManipulationError.Underflow; 308 | } 309 | 310 | if (self.next_idx == 1) { 311 | return Types.PeekPair{ 312 | .near = &self.contents[0].?, 313 | .far = if (self.prev) |prev| 314 | &prev.contents[prev.next_idx - 1].? 315 | else 316 | null, 317 | }; 318 | } 319 | 320 | return Types.PeekPair{ 321 | .near = &self.contents[self.next_idx - 1].?, 322 | .far = &self.contents[self.next_idx - 2].?, 323 | }; 324 | } 325 | 326 | test "do_peek_pair" { 327 | const stack = try Self.init(testAllocator, null); 328 | defer stack.deinit_guard_for_empty(); 329 | 330 | try expectError(StackManipulationError.Underflow, stack.do_peek_pair()); 331 | 332 | var target = try stack.do_push_uint(1); 333 | const top_one = try target.do_peek_pair(); 334 | try expectEqual(@as(usize, 1), top_one.near.*.UnsignedInt); 335 | try expectEqual(@as(?*Object, null), top_one.far); 336 | 337 | target = try target.do_push_uint(2); 338 | const top_two = try target.do_peek_pair(); 339 | try expectEqual(@as(usize, 2), top_two.near.*.UnsignedInt); 340 | try expectEqual(@as(usize, 1), top_two.far.?.*.UnsignedInt); 341 | 342 | target = try target.do_drop(); 343 | target = try target.do_drop(); 344 | } 345 | 346 | pub inline fn do_peek(self: *Self) !*Object { 347 | return (try self.do_peek_pair()).near; 348 | } 349 | 350 | /// Remove the top item off of this stack and return it, along with a 351 | /// pointer to which Stack object to perform future operations on. If this 352 | /// is the bottom Stack and there are no contents remaining, an Underflow 353 | /// is raised. 354 | pub fn do_pop(self: *Self) !Types.PopSingle { 355 | if (self.next_idx == 0) { 356 | if (self.prev) |prev| { 357 | self.deinit(); 358 | return prev.do_pop(); 359 | } 360 | 361 | return StackManipulationError.Underflow; 362 | } 363 | 364 | self.next_idx -= 1; 365 | if (self.contents[self.next_idx]) |obj| { 366 | self.contents[self.next_idx] = null; 367 | return Types.PopSingle{ 368 | .item = obj, 369 | .now_top_stack = self, 370 | }; 371 | } 372 | 373 | unreachable; 374 | } 375 | 376 | test "do_pop" { 377 | const stack = try Self.init(testAllocator, null); 378 | defer stack.deinit_guard_for_empty(); 379 | var target = try stack.do_push_uint(41); 380 | target = try stack.do_push_uint(42); 381 | const pop_42 = try target.do_pop(); 382 | try expectEqual(@as(usize, 42), pop_42.item.UnsignedInt); 383 | target = pop_42.now_top_stack; 384 | const pop_41 = try target.do_pop(); 385 | try expectEqual(@as(usize, 41), pop_41.item.UnsignedInt); 386 | target = pop_41.now_top_stack; 387 | try expectError(StackManipulationError.Underflow, target.do_pop()); 388 | } 389 | 390 | /// Remove the top two items off of this stack and return them, along with 391 | /// a pointer to which Stack object to perform future operations on. If 392 | /// this is the bottom Stack and there aren't at least two Objects 393 | /// remaining, an Underflow is raised. If this happens with one Object on 394 | /// the Stack, it will remain there. 395 | pub fn do_pop_pair(self: *Self) !Types.PopPair { 396 | const top_two = try self.do_peek_pair(); 397 | 398 | if (top_two.far) |_| { 399 | const near_pop = try self.do_pop(); 400 | const far_pop = try near_pop.now_top_stack.do_pop(); 401 | 402 | return Types.PopPair{ 403 | .near = near_pop.item, 404 | .far = far_pop.item, 405 | .now_top_stack = far_pop.now_top_stack, 406 | }; 407 | } 408 | 409 | return StackManipulationError.Underflow; 410 | } 411 | 412 | test "do_pop_pair" { 413 | const stack = try Self.init(testAllocator, null); 414 | defer stack.deinit_guard_for_empty(); 415 | 416 | var target = try stack.do_push_uint(41); 417 | target = try stack.do_push_uint(42); 418 | const pairing = try target.do_pop_pair(); 419 | try expectEqual(@as(usize, 42), pairing.near.UnsignedInt); 420 | try expectEqual(@as(usize, 41), pairing.far.UnsignedInt); 421 | try expectError(StackManipulationError.Underflow, target.do_pop()); 422 | } 423 | 424 | /// Remove the top three items off of this stack and return them, along 425 | /// with a pointer to which Stack object to perform future operations on. 426 | /// If this is the bottom Stack and there aren't at least three Objects 427 | /// remaining, an Underflow is raised. If this happens with one or two 428 | /// Objects on the Stack, they will remain there. 429 | pub fn do_pop_trio(self: *Self) !Types.PopTrio { 430 | const top_three = try self.do_peek_trio(); 431 | 432 | if (top_three.farther) |_| { 433 | const near_pop = try self.do_pop(); 434 | const far_pop = try near_pop.now_top_stack.do_pop(); 435 | const farther_pop = try far_pop.now_top_stack.do_pop(); 436 | 437 | return Types.PopTrio{ 438 | .near = near_pop.item, 439 | .far = far_pop.item, 440 | .farther = farther_pop.item, 441 | .now_top_stack = farther_pop.now_top_stack, 442 | }; 443 | } 444 | 445 | return StackManipulationError.Underflow; 446 | } 447 | 448 | test "do_pop_trio" { 449 | const stack = try Self.init(testAllocator, null); 450 | defer stack.deinit_guard_for_empty(); 451 | 452 | var target = try stack.do_push_uint(40); 453 | target = try stack.do_push_uint(41); 454 | target = try stack.do_push_uint(42); 455 | 456 | const trio = try target.do_pop_trio(); 457 | try expectEqual(@as(usize, 42), trio.near.UnsignedInt); 458 | try expectEqual(@as(usize, 41), trio.far.UnsignedInt); 459 | try expectEqual(@as(usize, 40), trio.farther.UnsignedInt); 460 | } 461 | 462 | /// Pushes an object to the top of this stack or a newly-created stack, as 463 | /// necessary based on available space. Returns pointer to whichever stack 464 | /// the object ended up on. 465 | pub fn do_push(self: *Self, obj: Object) !*Self { 466 | try self.non_terminal_stack_guard(); 467 | const target = try self.expand_to_fit(1) orelse self; 468 | target.contents[target.next_idx] = try obj.ref(); 469 | target.next_idx += 1; 470 | return target; 471 | } 472 | 473 | /// Push a managed array pointer onto this Stack as an Object. 474 | pub inline fn do_push_array(self: *Self, item: *Types.HeapedArray) !*Self { 475 | return try self.do_push(Object{ .Array = item }); 476 | } 477 | 478 | /// Push a Zig boolean value onto this Stack as an Object. 479 | pub inline fn do_push_bool(self: *Self, item: bool) !*Self { 480 | return try self.do_push(Object{ .Boolean = item }); 481 | } 482 | 483 | /// Push a Zig floating-point value onto this Stack as an Object. 484 | pub inline fn do_push_float(self: *Self, item: f64) !*Self { 485 | return try self.do_push(Object{ .Float = item }); 486 | } 487 | 488 | /// Push a Zig signed integer value onto this Stack as an Object. 489 | pub inline fn do_push_sint(self: *Self, number: isize) !*Self { 490 | return try self.do_push(Object{ .SignedInt = number }); 491 | } 492 | 493 | /// Push a managed string pointer onto this Stack as an Object. 494 | pub inline fn do_push_string(self: *Self, string: *Types.HeapedString) !*Self { 495 | return try self.do_push(Object{ .String = string }); 496 | } 497 | 498 | /// Push a managed symbol pointer onto this Stack as an Object. 499 | pub inline fn do_push_symbol(self: *Self, symbol: *Types.HeapedSymbol) !*Self { 500 | return try self.do_push(Object{ .Symbol = symbol }); 501 | } 502 | 503 | /// Push a Zig unsigned integer value onto this Stack as an Object. 504 | pub inline fn do_push_uint(self: *Self, number: usize) !*Self { 505 | return try self.do_push(Object{ .UnsignedInt = number }); 506 | } 507 | 508 | /// Push a managed word pointer onto this Stack as an Object. 509 | pub inline fn do_push_word(self: *Self, word: *Types.HeapedWord) !*Self { 510 | return try self.do_push(Object{ .Word = word }); 511 | } 512 | 513 | test "do_push" { 514 | const baseStack = try Self.init(testAllocator, null); 515 | defer baseStack.deinit(); 516 | 517 | // First, fill the current stack 518 | var i: usize = 0; 519 | while (i < STACK_SIZE) { 520 | try expectEqual(baseStack, try baseStack.do_push_uint(42)); 521 | i += 1; 522 | } 523 | try expectEqual(@as(usize, 42), baseStack.contents[baseStack.next_idx - STACK_SIZE / 2].?.UnsignedInt); 524 | try expectEqual(@as(usize, 42), baseStack.contents[baseStack.next_idx - 1].?.UnsignedInt); 525 | try expectEqual(@as(usize, STACK_SIZE), baseStack.next_idx); 526 | 527 | // Now force a new one to be allocated 528 | try expect(try baseStack.do_push_uint(42) != baseStack); 529 | try expectEqual(@as(usize, 1), baseStack.next.?.next_idx); 530 | } 531 | 532 | /// Duplicate the top Object on this stack via do_peek and do_push, 533 | /// returning a pointer to the Stack the Object ended up on. 534 | pub fn do_dup(self: *Self) !*Self { 535 | try self.non_terminal_stack_guard(); 536 | // By implementing in terms of do_push we get Rc(_) incrementing for 537 | // free 538 | return try self.do_push((try self.do_peek_pair()).near.*); 539 | } 540 | 541 | test "do_dup" { 542 | const stack = try Self.init(testAllocator, null); 543 | defer stack.deinit_guard_for_empty(); 544 | 545 | var target = try stack.do_push_uint(42); 546 | target = try stack.do_dup(); 547 | 548 | try expectEqual(@as(usize, 2), stack.next_idx); 549 | const top_two = try stack.do_pop_pair(); 550 | try expectEqual(@as(usize, 42), top_two.near.UnsignedInt); 551 | try expectEqual(@as(usize, 42), top_two.far.UnsignedInt); 552 | } 553 | 554 | pub fn do_2dupshuf(self: *Self) !*Self { 555 | try self.non_terminal_stack_guard(); 556 | const top_two = try self.do_peek_pair(); 557 | 558 | if (top_two.far == null) return StackManipulationError.Underflow; 559 | 560 | // TODO: avoid some overhead of jumping to do_push here by doing a 561 | // self.expand_to_fit(2) and handling the edge case I'm too lazy to 562 | // deal with right now where n+1 fits on the current stack, but n+2 563 | // forces a new allocation (split-stack case) 564 | // 565 | // technically the current implementation is "safest", since it'll do a 566 | // terminal guard each time, ensuring that we actually use target for 567 | // the second push, and not self... 568 | var target = try self.do_push(top_two.far.?.*); 569 | return try target.do_push(top_two.near.*); 570 | } 571 | 572 | test "do_2dupshuf" { 573 | const stack = try Self.init(testAllocator, null); 574 | defer stack.deinit_guard_for_empty(); 575 | 576 | var target = try stack.do_push_uint(420); 577 | target = try stack.do_push_uint(69); 578 | target = try stack.do_2dupshuf(); 579 | 580 | try expectEqual(@as(usize, 4), target.next_idx); 581 | const top_three = try target.do_pop_trio(); 582 | try expectEqual(@as(usize, 69), top_three.near.UnsignedInt); 583 | try expectEqual(@as(usize, 420), top_three.far.UnsignedInt); 584 | try expectEqual(@as(usize, 69), top_three.farther.UnsignedInt); 585 | target = top_three.now_top_stack; 586 | 587 | const top = try target.do_pop(); 588 | try expectEqual(@as(usize, 420), top.item.UnsignedInt); 589 | target = top.now_top_stack; 590 | 591 | try expectError(StackManipulationError.Underflow, target.do_pop()); 592 | } 593 | 594 | pub fn do_swap(self: *Self) StackManipulationError!void { 595 | try self.non_terminal_stack_guard(); 596 | return try @call( 597 | .{ .modifier = .always_inline }, 598 | self.do_swap_no_really_even_on_inner_stacks, 599 | .{}, 600 | ); 601 | } 602 | 603 | pub fn do_swap_no_really_even_on_inner_stacks(self: *Self) StackManipulationError!void { 604 | if (self.next_idx < 2 and self.prev == null) { 605 | return StackManipulationError.Underflow; 606 | } 607 | 608 | if (self.next_idx < 2 and self.prev.?.next_idx == 0) { 609 | unreachable; 610 | } 611 | 612 | const near_obj = &self.contents[self.next_idx - 1]; 613 | const far_obj = if (self.next_idx > 1) 614 | &self.contents[self.next_idx - 2] 615 | else 616 | &self.prev.?.contents[self.prev.?.next_idx - 1]; 617 | 618 | std.mem.swap(?Object, near_obj, far_obj); 619 | } 620 | 621 | test "do_swap: single stack" { 622 | const stack = try Self.init(testAllocator, null); 623 | defer stack.deinit_guard_for_empty(); 624 | 625 | var target = try stack.do_push_uint(1); 626 | target = try target.do_push_uint(2); 627 | try target.do_swap(); 628 | 629 | const top_two = try target.do_pop_pair(); 630 | try expectEqual(@as(usize, 2), top_two.far.UnsignedInt); 631 | try expectEqual(@as(usize, 1), top_two.near.UnsignedInt); 632 | } 633 | 634 | test "do_swap: transcend stack boundaries" { 635 | const baseStack = try Self.init(testAllocator, null); 636 | defer baseStack.deinit(); 637 | 638 | // First, fill the current stack 639 | var i: usize = 0; 640 | while (i < STACK_SIZE) { 641 | // Ensure that none of these operations will result in a new stack 642 | // being allocated. 643 | try expectEqual(baseStack, try baseStack.do_push_uint(1)); 644 | i += 1; 645 | } 646 | // Now force a new one to be allocated with a single, different object 647 | // on it. 648 | const newStack = try baseStack.do_push_uint(2); 649 | try expect(baseStack != newStack); 650 | 651 | // Save us from ourselves if we call swap on the wrong stack 652 | // (presumably, we discarded the output of do_push). 653 | try expectError( 654 | StackManipulationError.YouAlmostCertainlyDidNotMeanToUseThisNonTerminalStack, 655 | baseStack.do_swap(), 656 | ); 657 | 658 | try newStack.do_swap(); 659 | const top_two = try newStack.do_pop_pair(); 660 | try expectEqual(@as(usize, 2), top_two.far.UnsignedInt); 661 | try expectEqual(@as(usize, 1), top_two.near.UnsignedInt); 662 | } 663 | 664 | /// Remove the top item off of this stack. Return a pointer to which Stack 665 | /// object to perform future operations on. If this is the bottom Stack and 666 | /// there are no contents remaining, an Underflow is raised. If this is not 667 | /// the top Stack, a very wordy error is raised to save callers from 668 | /// usually-bug-derived-and-incorrect risky behavior; if you're absolutely 669 | /// sure you know what you're doing, see 670 | /// `do_drop_no_really_even_on_inner_stacks` instead. 671 | pub fn do_drop(self: *Self) !*Self { 672 | try self.non_terminal_stack_guard(); 673 | return try @call( 674 | .{ .modifier = .always_inline }, 675 | self.do_drop_no_really_even_on_inner_stacks, 676 | .{}, 677 | ); 678 | } 679 | 680 | /// Remove the top item off of this stack. Return a pointer to which Stack 681 | /// object to perform future operations on. If this is the bottom Stack and 682 | /// there are no contents remaining, an Underflow is raised. 683 | pub fn do_drop_no_really_even_on_inner_stacks(self: *Self) !*Self { 684 | var dropped = try self.do_pop(); 685 | 686 | // TODO: this currently relies on an assumption that Stack and Runtime 687 | // share the same allocator *instance*, which is only somewhat 688 | // accidentally true. The type signatures of these structs or 689 | // initialization sequencing should be used to guarantee this, or 690 | // Runtime should be an expected argument to this function. 691 | dropped.item.deinit(self.alloc); 692 | 693 | return dropped.now_top_stack; 694 | } 695 | 696 | // The auto-freeing mechanics of do_drop are being tested here, so 697 | // explicitly no defer->free() setups here *except* for the Stack itself. 698 | test "do_drop" { 699 | const hello_world = "Hello World!"; 700 | var str = try testAllocator.alloc(u8, hello_world.len); 701 | std.mem.copy(u8, str[0..], hello_world); 702 | var shared_str = try testAllocator.create(Types.HeapedString); 703 | shared_str.* = Types.HeapedString.init(str); 704 | 705 | const stack = try Self.init(testAllocator, null); 706 | defer stack.deinit_guard_for_empty(); 707 | 708 | var target = try stack.do_push_string(shared_str); 709 | target = try target.do_drop(); 710 | try expectEqual(@as(usize, 0), target.next_idx); 711 | try expectError(StackManipulationError.Underflow, target.do_pop()); 712 | } 713 | }; 714 | 715 | test { 716 | std.testing.refAllDecls(@This()); 717 | } 718 | -------------------------------------------------------------------------------- /lib/gale/test_gale.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | test "gale library test suite" { 15 | const std = @import("std"); 16 | std.testing.refAllDecls(@This()); 17 | 18 | _ = @import("./gale.zig"); 19 | _ = @import("./helpers.zig"); 20 | _ = @import("./internal_error.zig"); 21 | _ = @import("./nucleus_words.zig"); 22 | _ = @import("./object.zig"); 23 | _ = @import("./parsed_word.zig"); 24 | _ = @import("./rc.zig"); 25 | _ = @import("./runtime.zig"); 26 | _ = @import("./shape.zig"); 27 | _ = @import("./stack.zig"); 28 | _ = @import("./types.zig"); 29 | _ = @import("./word.zig"); 30 | _ = @import("./word_list.zig"); 31 | _ = @import("./word_map.zig"); 32 | _ = @import("./word_signature.zig"); 33 | 34 | // TODO: is this file actually needed? 35 | _ = @import("./type_system_tests.zig"); 36 | } 37 | -------------------------------------------------------------------------------- /lib/gale/test_helpers.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const Runtime = @import("./runtime.zig").Runtime; 15 | 16 | pub fn push_one(runtime: *Runtime) anyerror!void { 17 | try runtime.stack_push_uint(1); 18 | } 19 | 20 | pub fn push_two(runtime: *Runtime) anyerror!void { 21 | try runtime.stack_push_uint(2); 22 | } 23 | -------------------------------------------------------------------------------- /lib/gale/type_system_tests.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const Allocator = std.mem.Allocator; 16 | const testAllocator: Allocator = std.testing.allocator; 17 | const expect = std.testing.expect; 18 | const expectEqual = std.testing.expectEqual; 19 | const expectEqualStrings = std.testing.expectEqualStrings; 20 | const expectError = std.testing.expectError; 21 | 22 | const _shape = @import("./shape.zig"); 23 | const InternalError = @import("./internal_error.zig").InternalError; 24 | const Types = @import("./types.zig"); 25 | const WordSignature = @import("./word_signature.zig"); 26 | -------------------------------------------------------------------------------- /lib/gale/types.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | 16 | const Object = @import("./object.zig").Object; 17 | const Rc = @import("./rc.zig").Rc; 18 | const Stack = @import("./stack.zig").Stack; 19 | const Word = @import("./word.zig").Word; 20 | 21 | test { 22 | std.testing.refAllDecls(@This()); 23 | } 24 | 25 | pub const ObjectArray = std.ArrayList(Object); 26 | 27 | pub const HeapedArray = Rc(ObjectArray); 28 | pub const HeapedOpaque = Rc([]u8); 29 | pub const HeapedString = Rc([]u8); 30 | pub const HeapedSymbol = Rc([]u8); 31 | pub const HeapedWord = Rc(Word); 32 | 33 | pub const PopSingle = struct { 34 | item: Object, 35 | now_top_stack: *Stack, 36 | }; 37 | 38 | pub const PeekPair = struct { 39 | near: *Object, 40 | far: ?*Object, 41 | }; 42 | 43 | pub const PopPair = struct { 44 | near: Object, 45 | far: Object, 46 | now_top_stack: *Stack, 47 | }; 48 | 49 | pub const PopPairExternal = struct { 50 | near: Object, 51 | far: Object, 52 | }; 53 | 54 | pub const PeekTrio = struct { 55 | near: *Object, 56 | far: ?*Object, 57 | farther: ?*Object, 58 | }; 59 | 60 | pub const PopTrio = struct { 61 | near: Object, 62 | far: Object, 63 | farther: Object, 64 | now_top_stack: *Stack, 65 | }; 66 | 67 | pub const PopTrioExternal = struct { 68 | near: Object, 69 | far: Object, 70 | farther: Object, 71 | }; 72 | -------------------------------------------------------------------------------- /lib/gale/well_known_entities.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | 16 | const Runtime = @import("./runtime.zig").Runtime; 17 | const Shape = @import("./shape.zig").Shape; 18 | const WordSignature = @import("./word_signature.zig").WordSignature; 19 | 20 | // TODO: Should these be pointers to a Shape pool much like the signature 21 | // situation below? 22 | pub const WellKnownShapeStorage = [std.meta.fields(WellKnownShape).len]Shape; 23 | pub const WellKnownSignatureStorage = [std.meta.fields(WellKnownSignature).len]*WordSignature; 24 | 25 | /// A well-known Shape ships as part of the Runtime because it reflects a 26 | /// language primitive, rather than anything created in userspace. 27 | pub const WellKnownShape = enum(u8) { 28 | UnboundedArray = 0, 29 | UnboundedBoolean, 30 | UnboundedString, 31 | UnboundedSymbol, 32 | UnboundedUnsignedInt, 33 | UnboundedSignedInt, 34 | UnboundedFloat, 35 | UnboundedWord, 36 | UnboundedWordSignature, 37 | }; 38 | 39 | pub const WellKnownSignature = enum(u8) { 40 | NullarySingleUnboundedArray = 0, 41 | NullarySingleUnboundedBoolean, 42 | NullarySingleUnboundedString, 43 | NullarySingleUnboundedSymbol, 44 | NullarySingleUnboundedUnsignedInt, 45 | NullarySingleUnboundedSignedInt, 46 | NullarySingleUnboundedFloat, 47 | NullarySingleUnboundedWord, 48 | NullarySingleUnboundedWordSignature, 49 | }; 50 | 51 | pub fn shape_storage() WellKnownShapeStorage { 52 | return .{ 53 | // This explicit cast avoids an error at array fill time: 54 | // error: expected type '@TypeOf(undefined)', found 'shape.Shape' 55 | @as(Shape, undefined), 56 | } ** @typeInfo(WellKnownShapeStorage).Array.len; 57 | } 58 | 59 | pub fn signature_storage() WellKnownSignatureStorage { 60 | return .{ 61 | // This explicit cast avoids an error at array fill time: 62 | // error: expected type '@TypeOf(undefined)', found 'shape.Shape' 63 | @as(*WordSignature, undefined), 64 | } ** @typeInfo(WellKnownSignatureStorage).Array.len; 65 | } 66 | 67 | pub fn populate(rt: *Runtime) !void { 68 | for (rt.well_known_shapes) |*it, idx| { 69 | switch (@intToEnum(WellKnownShape, idx)) { 70 | .UnboundedArray => { 71 | it.* = Shape.new_containing_primitive(.Unbounded, .Array); 72 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 73 | stored.value_ptr.* = {}; 74 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedArray)] = 75 | stored.key_ptr; 76 | }, 77 | .UnboundedBoolean => { 78 | it.* = Shape.new_containing_primitive(.Unbounded, .Boolean); 79 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 80 | stored.value_ptr.* = {}; 81 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedBoolean)] = 82 | stored.key_ptr; 83 | }, 84 | .UnboundedString => { 85 | it.* = Shape.new_containing_primitive(.Unbounded, .CharSlice); 86 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 87 | stored.value_ptr.* = {}; 88 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedString)] = 89 | stored.key_ptr; 90 | }, 91 | .UnboundedSymbol => { 92 | it.* = Shape.new_containing_primitive(.Unbounded, .CharSlice); 93 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 94 | stored.value_ptr.* = {}; 95 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedSymbol)] = 96 | stored.key_ptr; 97 | }, 98 | .UnboundedUnsignedInt => { 99 | it.* = Shape.new_containing_primitive(.Unbounded, .UnsignedInt); 100 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 101 | stored.value_ptr.* = {}; 102 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedUnsignedInt)] = 103 | stored.key_ptr; 104 | }, 105 | .UnboundedSignedInt => { 106 | it.* = Shape.new_containing_primitive(.Unbounded, .SignedInt); 107 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 108 | stored.value_ptr.* = {}; 109 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedSignedInt)] = 110 | stored.key_ptr; 111 | }, 112 | .UnboundedFloat => { 113 | it.* = Shape.new_containing_primitive(.Unbounded, .Float); 114 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 115 | stored.value_ptr.* = {}; 116 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedFloat)] = 117 | stored.key_ptr; 118 | }, 119 | .UnboundedWord => { 120 | it.* = Shape.new_containing_primitive(.Unbounded, .Word); 121 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 122 | stored.value_ptr.* = {}; 123 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedWord)] = 124 | stored.key_ptr; 125 | }, 126 | .UnboundedWordSignature => { 127 | it.* = Shape.new_containing_primitive(.Unbounded, .WordSignature); 128 | var stored = try rt.signatures.getOrPut(WordSignature{ .NullarySingle = it }); 129 | stored.value_ptr.* = {}; 130 | rt.well_known_signatures[@enumToInt(WellKnownSignature.NullarySingleUnboundedWordSignature)] = 131 | stored.key_ptr; 132 | }, 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/gale/word.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | 16 | const Object = @import("./object.zig").Object; 17 | const Runtime = @import("./runtime.zig").Runtime; 18 | const Types = @import("./types.zig"); 19 | const WordSignature = @import("./word_signature.zig").WordSignature; 20 | 21 | // TODO: This should almost certainly not be anyerror. 22 | // TODO: handle stack juggling 23 | pub const PrimitiveWord = fn (*Runtime) anyerror!void; 24 | 25 | pub const CompoundImplementation = []*Types.HeapedWord; 26 | pub const HeapLitImplementation = *Object; 27 | pub const PrimitiveImplementation = *const PrimitiveWord; 28 | pub const WordImplementation = union(enum) { 29 | Primitive: PrimitiveImplementation, 30 | Compound: CompoundImplementation, 31 | HeapLit: HeapLitImplementation, 32 | }; 33 | 34 | pub const Flags = packed struct { 35 | hidden: bool, 36 | }; 37 | 38 | // TODO: Docs. 39 | pub const Word = struct { 40 | const Self = @This(); 41 | 42 | pub const SignatureState = union(enum) { 43 | Declared: *WordSignature, 44 | Inferred: *WordSignature, 45 | }; 46 | 47 | // Those finding they need more tag space should compile their own 48 | // project-specific gale build changing the constant as appropriate. Unlike 49 | // many languages where mucking about with the internals is faux-pas, in 50 | // gale it is encouraged on a "if you know you really need it" basis. 51 | // 52 | // TODO: configurable in build.zig 53 | pub const MAX_GLOBAL_TAGS = 256; 54 | pub const TAG_ARRAY_SIZE = MAX_GLOBAL_TAGS / 8; 55 | 56 | flags: Flags, 57 | 58 | /// Null state generally represents a word in construction; finding a null 59 | /// .signature in the wild is fairly certainly an implementation bug. 60 | signature: ?SignatureState, 61 | 62 | // This is thinking ahead for functionality I want to be able to provide: 63 | // while gale is in no way a "pure functional" language like Haskell, and 64 | // while I frankly don't know enough about type systems or their theory to 65 | // (currently) implement an effects tracking system, it'd still be nice to 66 | // be able to signal to callers (of the human variety) in their dev 67 | // environments, "hey, this method calls out to IO!". Tags could then be 68 | // transitive through a word stack, so a "main" word that calls "getenv" 69 | // and "println" (or whatever those functions end up called) would get 70 | // "tainted" with the 'IO tag. 71 | // 72 | // The thought behind limiting global tags is that it forces judicial use 73 | // of these things. Given that Words are the fundamental mechanism for 74 | // computing, there will be *many* of these things, and so they will become 75 | // memory-costly if unbounded. 76 | // 77 | // These are simple bitmasks, and so with the default of 256 global tags, 78 | // we'll use 32 bytes per word. 79 | // 80 | // TODO: a pointer is only 8 bytes (or 4 on x32), does it make more sense 81 | // to save 24 bytes of RAM at the cost of a pointer jump whenever we care 82 | // about accessing these tags? I assume (upfront assumptions in language 83 | // design, lolololol) we'll only really care about these in the 84 | // LSP/devtools/debugger/etc. cases, and almost never at runtime. Further, 85 | // this could allow memory deduplication by storing each tags set exactly 86 | // once (getOrPut pattern as with hash maps etc.), which is almost 87 | // certainly a useful property. 88 | tags: [TAG_ARRAY_SIZE]u8, 89 | 90 | impl: WordImplementation, 91 | 92 | pub fn new_untagged(impl: WordImplementation, sig: ?SignatureState) Self { 93 | return Self{ 94 | .flags = .{ .hidden = false }, 95 | .tags = [_]u8{0} ** TAG_ARRAY_SIZE, 96 | .impl = impl, 97 | .signature = sig, 98 | }; 99 | } 100 | 101 | pub fn new_compound_untagged( 102 | impl: CompoundImplementation, 103 | sig: ?SignatureState, 104 | ) Self { 105 | return new_untagged(.{ .Compound = impl }, sig); 106 | } 107 | 108 | pub fn new_heaplit_untagged( 109 | impl: HeapLitImplementation, 110 | sig: ?SignatureState, 111 | ) Self { 112 | return new_untagged(.{ .HeapLit = impl }, sig); 113 | } 114 | 115 | pub fn new_primitive_untagged( 116 | impl: PrimitiveImplementation, 117 | sig: ?SignatureState, 118 | ) Self { 119 | return new_untagged(.{ .Primitive = impl }, sig); 120 | } 121 | 122 | pub fn deinit(self: *Self, alloc: std.mem.Allocator) void { 123 | return switch (self.impl) { 124 | // There's no necessary action to deinit a primitive: they live in 125 | // the data sector of the binary anyway and can't be freed. 126 | .Primitive => {}, 127 | // Defer to the heaped object's teardown process for its underlying 128 | // memory as appropriate, and then destroy the Rc that holds that 129 | // Object. 130 | .HeapLit => |obj| { 131 | obj.deinit(alloc); 132 | alloc.destroy(obj); 133 | }, 134 | .Compound => |compound| { 135 | // First, loop through and release our holds on each of these 136 | // Rcs and free the inner memory as appropriate, but do not 137 | // destroy the Rc itself yet: if there are duplicates in our 138 | // slice, destroyed Rcs become segfaults, and segfaults are 139 | // sad. 140 | for (compound) |iword| { 141 | if (!iword.dead()) { 142 | _ = iword.decrement_and_prune(.DeinitInnerWithAlloc, alloc); 143 | } 144 | } 145 | 146 | // Now loop back through and destroy any orphaned Rc objects. 147 | for (compound) |iword| { 148 | if (iword.dead()) { 149 | alloc.destroy(iword); 150 | } 151 | } 152 | 153 | // Finally, destroy the compound slice itself. 154 | alloc.free(compound); 155 | }, 156 | }; 157 | } 158 | }; 159 | 160 | test { 161 | std.testing.refAllDecls(@This()); 162 | } 163 | -------------------------------------------------------------------------------- /lib/gale/word_list.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const Allocator = std.mem.Allocator; 16 | 17 | const Types = @import("./types.zig"); 18 | 19 | /// A managed std.ArrayList of *Types.HeapedWords. 20 | pub const WordList = struct { 21 | const Self = @This(); 22 | const Inner = std.ArrayList(*Types.HeapedWord); 23 | 24 | contents: Inner, 25 | 26 | pub fn init(alloc: Allocator) Self { 27 | return Self{ 28 | .contents = Inner.init(alloc), 29 | }; 30 | } 31 | 32 | /// Destroy all contents of self (following the nested garbage collection 33 | /// rules discussed in `Rc.decrement_and_prune`'s documentation) and any 34 | /// overhead metadata incurred along the way. 35 | pub fn deinit(self: *Self, alloc: Allocator) void { 36 | while (self.contents.popOrNull()) |entry| { 37 | _ = entry.decrement_and_prune(.DeinitInnerWithAllocDestroySelf, alloc); 38 | } 39 | self.contents.deinit(); 40 | } 41 | 42 | pub fn items(self: *Self) []*Types.HeapedWord { 43 | return self.contents.items; 44 | } 45 | 46 | pub fn len(self: *Self) usize { 47 | return self.contents.items.len; 48 | } 49 | 50 | pub fn append(self: *Self, item: *Types.HeapedWord) Allocator.Error!void { 51 | return try self.contents.append(item); 52 | } 53 | }; 54 | 55 | test { 56 | std.testing.refAllDecls(@This()); 57 | } 58 | -------------------------------------------------------------------------------- /lib/gale/word_map.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | 16 | const Types = @import("./types.zig"); 17 | const WordList = @import("./word_list.zig").WordList; 18 | 19 | const SymbolContext = struct { 20 | const Self = @This(); 21 | 22 | pub fn hash(_: Self, s: *Types.HeapedSymbol) u64 { 23 | return std.hash_map.hashString(s.value.?); 24 | } 25 | pub fn eql(_: Self, a: *Types.HeapedSymbol, b: *Types.HeapedSymbol) bool { 26 | return std.hash_map.eqlString(a.value.?, b.value.?); 27 | } 28 | }; 29 | 30 | // TODO: Docs. 31 | pub const WordMap = std.HashMap( 32 | *Types.HeapedSymbol, 33 | WordList, 34 | SymbolContext, 35 | std.hash_map.default_max_load_percentage, 36 | ); 37 | 38 | test { 39 | std.testing.refAllDecls(@This()); 40 | } 41 | -------------------------------------------------------------------------------- /sketches/bounded_parameters.gale: -------------------------------------------------------------------------------- 1 | { Resolution will check bounded candidates before falling back to an unbounded; 2 | this unbounded fallback is required (there's no possible way to exhaustiveness 3 | check primitives: if you know all the possible values, you should be using an 4 | enum Shape anyway!) } 5 | 6 | { [_] attempts to infer the type... since we know eq?!! is 7 | ( @1 #1! @1 #2! -> @1 #2! Boolean ) and this anonymous word is not stack- 8 | balanced (requires one more @1==UnsignedInt on the stack to execute without 9 | underflow), [_] can be made (using the future inference engine that doesn't 10 | yet exist) to understand it needs to create a bounded UnsignedInt (where 11 | /in-bounds? then calls this anonymous word). 12 | 13 | The exact underlying semantics of how bounded/unbounded types work in Zig-space 14 | are yet-undecided, though I'm quickly reaching the point where I need to figure 15 | it out... } 16 | { This desugars to: ( [ 42 eq?! ] [_] -> Boolean ) } 17 | : is-number-i-like? ( 42 [eq] -> Boolean ) true ; 18 | { Desugars: ( [ 9000 lt?! ] [_] -> Boolean ) } 19 | : is-number-i-like? ( 9000 [lt] -> Boolean ) true ; 20 | : is-number-i-like? ( UnsignedInt -> Boolean ) false ; 21 | 22 | : is-my-name? ( "klardotsh" [eq] -> Boolean ) true ; 23 | : is-my-name? ( [ "Josh" "Klar" eq-either? ] [_] -> Boolean ) true ; 24 | : is-my-name? ( String -> Boolean ) false ; 25 | 26 | { Bounded Structs are a bit of a fun one to solve for... } 27 | 28 | $ Foo 29 | $> bar UnsignedInt ; 30 | $> baz String ; 31 | ; 32 | 33 | { Matching just one field is relatively easy... } 34 | { Desugars to: ( [ .bar 42 eq?! ] [Foo] <- ) } 35 | : do-something ( 42 [Foo=>bar] <- ) "the answer to everything" println ; 36 | 37 | { Matching two fields there's no syntax sugar for, good luck! } 38 | { Sometimes it's nice to give names to complex things... } 39 | : is-special-something? ( Foo <- Boolean ) 40 | .bar 420 eq? 41 | ,.baz "loljk" eq? 42 | and! 43 | ; 44 | : do-something ( is-special-something? [Foo] <- ) .baz println ; 45 | : do-something ( Foo <- ) "useless Foo, please fight it" eprintln ; 46 | -------------------------------------------------------------------------------- /sketches/branching_overloading.gale: -------------------------------------------------------------------------------- 1 | { We can define freestanding words for generic shape members. 2 | This word is entirely silly, by the way: use Optional/map-or } 3 | : freestanding-repr ( Optional.None -> String ) "" ; 4 | { Now that we've defined freestanding-repr for one branch of Optional, we must 5 | define it for all other branches } 6 | : freestanding-repr ( Optional.Some -> String ) Optional/unwrap repr ; 7 | 8 | : console-yeet freestanding-repr println ; 9 | 10 | {{ Prints "" to the console }} 11 | : demo-freestanding-repr-1 Optional/none console-yeet ; 12 | {{ Prints "Foo" to the console }} 13 | : demo-freestanding-repr-2 "Foo" Optional/some console-yeet ; 14 | {{ Prints "1" to the console }} 15 | : demo-freestanding-repr-3 1 Optional/some console-yeet ; 16 | 17 | { Now let's get a little weirder. What if we want to attach methods 18 | specifically to an Optional? 19 | 20 | First, helper methods for later. 21 | `extract` is a word from Extractable, which all enum members with constituent 22 | data derive from. Note that the only form of type narrowing available is 23 | through word signatures, and the net effects to the stack after execution of 24 | a word must be known at Build Time, so if an Enum has members holding an 25 | UnsignedInt and a String, say, `extract` won't work on the whole Enum: we 26 | must define words for each member type. } 27 | : unfold-to-console ( Result.Error ) extract eprintln ; 28 | : unfold-to-console ( Result.Ok ) extract println ; 29 | 30 | { Aliasing this shape is required to provide a name for `/` syntax to match to } 31 | $@ OptionalString String Optional! ; 32 | 33 | { This signature expands to `( String Self -> Result )` via this 34 | implied behavior and type inference. The inference engine isn't generally 35 | smart enough to figure out that a String is needed here (yet?), so we have to 36 | help it out. } 37 | : OptionalString/try-concat ( String Self ) 38 | { Anonymous word syntax with [ ]. Apply this word to Self if it is a Some, 39 | giving us a new Optional. These anonymous words execute in the 40 | context of whatever is calling them, there are no closures. In the case 41 | of Optional/map, in the event of an Optional.None, absolutely nothing 42 | happens and the anonymous word is ignored. In the event of an 43 | Optional.Some, the inner value is unwrapped onto the stack, consuming the 44 | Optional, and the anonymous word is immediately run. Anonymous words can 45 | never consume more stack than will be available to the outer word at the 46 | time of execution, and cannot contain nested anonymous words. } 47 | [ ", the world was gonna roll me" /prepend ] Optional/map 48 | 49 | "You can't concatenate nothing with something, silly!" 50 | Optional/ok-or 51 | ; 52 | 53 | : OptionalString/try-concat-to-console /try-concat unfold-to-console ; 54 | 55 | : lyric "Somebody once told me" ; 56 | 57 | {{ Prints "You can't concatenate...." to stderr }} 58 | : demo-attached-alias lyric Optional/none /try-concat-to-console ; 59 | {{ Prints "Somebody once told me, the world..." to stdout }} 60 | : demo-attached-alias lyric Optional/some /try-concat-to-console ; 61 | -------------------------------------------------------------------------------- /sketches/sameness_checker.gale: -------------------------------------------------------------------------------- 1 | : mangle 2 | ( String #1! -> String #2! ) 3 | {{ 4 | Returns a new String object based on the original, destroying the 5 | original in the process. 6 | }} 7 | "bar" append 8 | ,drop 9 | ; 10 | 11 | : main "foo" mangle println ; 12 | 13 | { Now let's explore all the ways we can make the checker angry! } 14 | 15 | : mangle-oops1 16 | ( String -> String ) 17 | "bar" append ,drop ; 18 | { ^ Not allowed! #1 is always implied unless overridden, and so 19 | we are obligated to leave the same string object on the stack. 20 | String/append is ( String #1! <- String #2! ) (non-destructive), 21 | drop is ( @1 -> ) } 22 | 23 | : mangle-oops2 24 | ( String #1! <- String #2! ) 25 | dup ; 26 | { ^ Not allowed! While we made a second String on the stack, it's the 27 | same string we already had and this is known by signature, because 28 | dup is ( @1 <- @1 ), implying #1 for both generics. } 29 | -------------------------------------------------------------------------------- /sketches/simple_shapes.gale: -------------------------------------------------------------------------------- 1 | { This is provided by the standard library, but is a useful reference to 2 | understand this file. } 3 | {{ Something that can be made representable as a String }} 4 | $ Printable 5 | $: repr ( Self <- String ) ; 6 | 7 | { ; is actually a :@ alias for ShapeAssembler/; (with signature `( Self <- 8 | nothing )`), which is implemented as ShapeDefStub/; (used below) and 9 | ShapeMemberStub/; (used above). 10 | 11 | :@ aliases *must* point to unambiguous word signatures, but since 12 | `ShapeAssembler/;` will resolve to `ShapeDefStub/;` if `Self` is a 13 | `ShapeDefStub` (since `ShapeDefStub` declares itself to be an implementation of 14 | `ShapeAssembler`), we get a very limited (or "controlled", depending on your 15 | viewpoint) implementation of "method overloading", without the full-on 16 | craziness in the language spec that would be required to allow multiple 17 | definitions of, say, the word `add` all using that same title. } 18 | ; 19 | 20 | { We could, but will not for now, provide a default implementation for repr 21 | like so. %? is like Zig's `{any}` or Rust's `{:?}`, and dumps the debug 22 | representation of the top object of the stack. 23 | 24 | : Printable/repr "%?" make-formatting-word ; 25 | } 26 | 27 | {{ A recorded piece of music, perhaps in a collection of other recordings. }} 28 | $ Track 29 | { This is an explicit opt-in to fulfil a Shape: the runtime won't let us pass 30 | Tracks into anything that expects Printables until we've fulfilled its 31 | contract. More on that later. } 32 | $. Printable ; 33 | 34 | { These are members: num and total are understood to be Symbols, and exactly 35 | one more thing should be on the stack by the time we get to ;: a shape 36 | reference. We're in Exec mode after "num", but that's okay: calling a 37 | Shape's name returns a reference to itself. } 38 | $> num UnsignedInt ; 39 | $> total UnsignedInt ; 40 | ; 41 | 42 | { This name enables some magic: this method will automatically be associated with 43 | the Track shape, though note that there is no requirement that it actually take 44 | Track off the stack: indeed, this word never will, taking two UnsignedInts 45 | instead. Think more like Zig's structs or Python's @staticmethod, and less like 46 | forced-OOP. 47 | 48 | :@ means that the word body will itself be a single word (words are 49 | first-class, like functions in JavaScript et. al.), and that that word should 50 | be hoisted to the provided name. This is some of the only metaprogramming 51 | allowed in Gale, and it follows rather strict rules to allow devtools to 52 | analyze the codebase even with these existing. } 53 | :@ Track/format "Track %d/%d" make-formatter ; 54 | 55 | : Track/repr { This has signature ( Self <- String ), but it's inferred! } 56 | { Trailing comma inverts the usual flow and puts total below the Track on 57 | the Stack } 58 | /.total, 59 | /.num, 60 | { Leading comma "hides" the top object on the stack from the word to 61 | follow, leading / on the word name means "look for a method named format 62 | associated with the shape of the top of the stack". This functionality 63 | ignores the leading comma's effect for lookup purposes, so our Track will 64 | be the shape used to look for a /format method. } 65 | ,/format 66 | 67 | { Stack is now ( Track String ) which is the correct result signature for 68 | this word, et voila! } 69 | ; 70 | 71 | { FORTHers might feel more at home one-lining that, which is totally legal too: 72 | : Track/repr /.total, /.num, ,/format ; 73 | } 74 | 75 | {{ The category of music a song is considered to fit. }} 76 | % Genre 77 | { These are all understood to be Symbols } 78 | %_ Rock ; 79 | %_ Metal ; 80 | %_ Electronic ; 81 | 82 | { But they can take exactly one member argument. Assume we had subgenre 83 | enums defined: 84 | 85 | %> Electronic ElectronicSubgenre ; 86 | } 87 | ; 88 | 89 | {{ A recorded piece of music and its metadata. }} 90 | $ Song 91 | $. artist String ; 92 | $. album_artist String Optional! ; 93 | $. title String ; 94 | $. track Track Optional! ; 95 | $. genres Genre List! ; 96 | ; 97 | 98 | :@ Song/format "%s by %s" make-formatter ; 99 | 100 | { By defining this on a Shape that hasn't opted into Printable explicitly, we 101 | are still eligible to be an *implicit* Printable: we won't get devtools 102 | yelling at us about missing word implementations until we've tried calling 103 | Printable/repr (or some other word that demands a Printable) with a Song on 104 | the top of the stack, but we *can* ad-hoc opt into Shapes anyway. } 105 | : Song/repr 106 | /.title, 107 | { Leading + trailing comma combination combines to mean we'll ignore the 108 | Song on top of the stack, null-coalesce the first extant string of either 109 | artist or album_artist, and slip that String into the stack under the 110 | Song, which will remain on top of the stack. } 111 | /.album_artist, /.artist, ,Optional/or, 112 | ,/format 113 | ; 114 | 115 | : main 116 | Genre.Electronic! { ! after an enum member instantiates one of it } 117 | 118 | { Understanding Track! might be non-trivial at first glance: Track! will 119 | eat things off the stack in the order they were defined in the Shape 120 | definition, so .num goes first and thus needs to be the *second* thing we 121 | push onto the stack. } 122 | 7 { .total } 123 | 5 { .num } 124 | Track! 125 | Optional/some { Pack the Track into an Optional } 126 | 127 | "Wandering Star" 128 | Optional/none 129 | "Portishead" 130 | 131 | Song! { Build a Song instance in field packing order } 132 | 133 | /repr println { This prints "Wandering Star by Portishead" } 134 | drop { And now let's tear down the Song object } 135 | ; 136 | 137 | { Alternatively, we could have written main using the symbol-based shape 138 | builder words. } 139 | : main-symbol-based 140 | Song do 141 | { 142 | :@ do Blockable/do ; 143 | : Blockable/do ( @1 Self! <- @1 Block! ) ; 144 | $ ShapeStub 145 | $< :TShape ; 146 | $. Blockable ; 147 | { ... among other things ... } 148 | ; 149 | : Shape/block-open ( Self <- Self ShapeStub! 150 | 151 | :@ = Packable/pack, which is implemented for 152 | ( SongStub String %Song/.artist ), 153 | ( SongStub String Optional! %Song/.album_artist ), etc. 154 | 155 | %Song is an auto-generated enum instance (of an "anonymous" shape) 156 | holding the member types of the Song struct. It contains member 157 | words, all starting with a `.`, which will generate a Shape 158 | reference to the Shape stored in the struct field in Song by the 159 | same name. This is mostly useful because word definitions must be 160 | exhaustive across all members of the enum %Song, thus ensuring 161 | we've implemented ShapeStub/pack for all fields of Song. This 162 | functionality is currently mostly useful to the language itself, 163 | but the power is there for end-developers to tinker with and find 164 | fun uses for. 165 | 166 | SongStub implements Packable, enabling all this. The "row" is 167 | stashed into the SongStub so that the stack is only one element 168 | taller when assigning values, meaning comma operators should make 169 | use of these constructors reasonably ergonomic. 170 | } 171 | "Portishead" :artist = 172 | Optional/none :album_artist = 173 | "Wandering Star" :title = 174 | 175 | { Nestable, and note also that enums are also Blockable, like 176 | any Shape, though the syntax here is probably less readable than just 177 | calling Optional/some or Optional.Some!. } 178 | Optional do Track do 5 :num 7 :total end :Some = end :track = 179 | { 180 | Alternatively: 181 | Track do 5 :num 7 :total end Optional/some :track ^ 182 | } 183 | end 184 | { ^ :@ end Blockable/end, which pops the ShapeStub back out into 185 | correctly-ordered objects on the main stack and calls /construct to 186 | instantiate the underlying shape. This is a build-mode word, and so 187 | verification that all members are present happens before the application 188 | runs. } 189 | ; 190 | -------------------------------------------------------------------------------- /sketches/union_types_instead_of_enums.gale: -------------------------------------------------------------------------------- 1 | { None is an intrinsic provided by the runtime, representing the lack of a 2 | value. } 3 | {{ Used to represent the presence of a value, in contrast to the intrinsic None, 4 | which represents the lack thereof. }} 5 | $> Some @1 ; 6 | $= Maybe Some None or ; 7 | 8 | {{ Used to represent a success state, wrapping an inner value (the data). }} 9 | $> Ok @1 ; 10 | {{ Used to represent a failure state, wrapping an inner value (the error). }} 11 | $> Err @1 ; 12 | $= Result Ok Err or ; 13 | -------------------------------------------------------------------------------- /src/gale/main.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | const gale = @import("gale"); 16 | 17 | pub fn main() anyerror!void { 18 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 19 | const runtime = try gale.Runtime.init(gpa.allocator()); 20 | std.debug.print("{any}\n", .{runtime}); 21 | } 22 | -------------------------------------------------------------------------------- /tests/test_protolang.zig: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Josh Klar aka "klardotsh" 2 | // 3 | // Permission to use, copy, modify, and/or distribute this software for any 4 | // purpose with or without fee is hereby granted. 5 | // 6 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | // PERFORMANCE OF THIS SOFTWARE. 13 | 14 | const std = @import("std"); 15 | 16 | test { 17 | std.testing.refAllDecls(@This()); 18 | } 19 | --------------------------------------------------------------------------------