]> rethought.computer Git - sorel-lang.git/commitdiff
structs main
authorBryan English <bryan@rethought.computer>
Wed, 26 Aug 2026 02:19:29 +0000 (22:19 -0400)
committerBryan English <bryan@rethought.computer>
Thu, 27 Aug 2026 02:43:54 +0000 (22:43 -0400)
docs/language_overview.md
sorel-parser/src/lib.rs
tests/test.sh
tests/test2.sorel
tests/test3.sorel [new file with mode: 0644]

index 1e3475a166b5ccccf15d47860fbb06a0510edee5..8228116e08e7c1f032da56a0ed55195fa2ed70a5 100644 (file)
@@ -27,6 +27,7 @@ Number literals can provide type information after a colon.
 For example, `-37:i32` will add -37 as a signed 32-bit integer onto the stack.
 Similarly, `45.5:f64` will add 45.5 as a 64-bit float, and `123:u8` will add 123 as an unsigned 8-bit integer.
 Numbers respect the system endianness (little-endian).
+These same suffixes are used in struct definitions (see below) with the addition of the `ptr` suffix.
 
 Values themselves don't have types.
 Operations have types.
@@ -103,6 +104,40 @@ In order to use `call`, you'll need to get a function pointer.
 These can be retrieved for a given function by prefixing it with an apostrophe `'`.
 For eample, to put a pointer to `foo` onto the stack, do `'foo`.
 
+## Structs
+
+It's often useful to interact with structured data, particularly in code that interacts in some way with other langauges.
+Sorel provides some syntactic sugar for dealing with structs as they would be defined in C.
+You can define a struct with the `::` word, followed by the struct name and fields, indicated as words of the form `field_name:type`, where the type is one of Sorel's core types like `u8`, `f32`, or `ptr`.
+Close out the struct with the `;` word, much like in word definitions.
+For easy interop with other native code, structs use the same memory layout as in C.
+
+Then, when you want to refer to a field in the struct, concatenate the struct name with the field name with a `.` to get the offset of that field.
+
+You can get the size of a struct in memory by prepending the struct name with `sizeof.`.
+
+For example:
+
+```
+:: my_struct
+  field1:u8
+  field2:u16
+;
+
+sizeof.my_struct
+\ `4` is now on the top of the stack
+
+\ now assume `instance` is a pointer to a my_struct
+
+instance my_struct.field2
+\ a pointer to `field2` of `instance` is now on top of the stack
+```
+
+Remember that structs are syntactic sugar, meaning they can't be imported or exported on their own.
+Only their generated words can (e.g. the specific field helper words or sizeof).
+An advantage of this is you can have effectively "private" fields by not exporting their generated words.
+Do _not_ rely on this for security though, since it's all just raw memory and offsets can be easily computed.
+
 ## Standard Library
 
 Built-in words are insufficient to create most programs.
index e269e062858e039ce3f11dd0fb63d7044b26a171..757d1b659498a39790438fdddaf07de2514dc92a 100644 (file)
@@ -3,7 +3,7 @@ use sorel_tokenizer::Token;
 
 #[derive(Debug)]
 pub struct WordDefinition<'a> {
-    pub name: &'a str,
+    pub name: String,
     pub instructions: Vec<Token<'a>>,
 }
 
@@ -22,12 +22,71 @@ enum LastMeta {
     None,
 }
 
+struct Struct<'a> {
+    name: &'a str,
+    fields: Vec<(&'a str, usize)>,
+}
+
+fn size_str_to_usize(size_str: &str) -> usize {
+    match size_str {
+        "u8" => 1,
+        "i8" => 1,
+        "u16" => 2,
+        "i16" => 2,
+        "u32" => 4,
+        "i32" => 4,
+        "u64" => 8,
+        "i64" => 8,
+        "ptr" => size_of::<usize>(),
+        _ => 0, // TODO should this error out instead?
+    }
+}
+
+impl<'a> Struct<'a> {
+    fn to_words(&'a self, module: &mut Module) {
+        let mut total_size = 0;
+
+        for (name, size) in self.fields.clone() {
+            let remainder = total_size % size;
+            let offset = if remainder == 0 {
+                total_size
+            } else {
+                total_size + (size - remainder)
+            };
+            module.words.push(WordDefinition {
+                name: format!("{}.{}", self.name, name),
+                instructions: vec![Token::NumU64(offset as u64), Token::Word("+")],
+            });
+            total_size = offset + size;
+        }
+
+        module.words.push(WordDefinition {
+            name: format!("sizeof.{}", self.name),
+            instructions: vec![Token::NumU64(total_size as u64)]
+        });
+    }
+
+    fn add_field(&mut self, token: Token<'a>) -> Result<()> {
+        if let Token::Word(token) = token {
+            let splat: Vec<_> = token.split(":").collect();
+            let name = splat[0];
+            let size_str = splat[1];
+            self.fields.push((name, size_str_to_usize(size_str)));
+            Ok(())
+        } else {
+            bail!("Improper struct definition!");
+        }
+    }
+}
+
 impl<'a> Module<'a> {
     pub fn parse(input: Vec<Token<'a>>, is_entrypoint: bool) -> Result<Self> {
         let mut module = Module::default();
         let mut main = vec![];
         let mut current_word: Option<WordDefinition> = None;
+        let mut current_struct: Option<Struct> = None;
         let mut last_was_colon = false;
+        let mut last_was_double_colon = false;
         let mut last_meta = LastMeta::None;
 
         for token in input {
@@ -35,26 +94,50 @@ impl<'a> Module<'a> {
                 // We're about to start defining a word, and the current token
                 // is the word's name.
                 current_word = Some(WordDefinition {
-                    name: token.as_word()?,
+                    name: token.as_word()?.to_string(),
                     instructions: vec![],
                 });
                 last_was_colon = false;
+
+                continue;
+            }
+            if last_was_double_colon {
+                // We're about to start defining a struct, and the current token
+                // is the struct's name.
+                current_struct = Some(Struct {
+                    name: token.as_word()?,
+                    fields: vec![],
+                });
+                last_was_double_colon = false;
+
                 continue;
             }
 
             if let Token::Word(word) = token {
                 if word == ":" {
-                    if current_word.is_some() {
-                        bail!("can't define words inside word definitions!");
+                    if current_word.is_some() || current_struct.is_some() {
+                        bail!("can't start definitions inside definitions!");
                     }
                     last_was_colon = true;
                     continue;
                 }
+                if word == "::" {
+                    if current_word.is_some() || current_struct.is_some() {
+                        bail!("can't start definitions inside definitions!");
+                    }
+                    last_was_double_colon = true;
+                    continue;
+                }
                 if word == ";" {
-                    let word = current_word
-                        .take()
-                        .expect("`;` must be at the end of a word definition");
-                    module.words.push(word);
+                    if let Some(word) = current_word {
+                        current_word = None;
+                        module.words.push(word);
+                    } else if let Some(strukt) = current_struct {
+                        current_struct = None;
+                        strukt.to_words(&mut module);
+                    } else {
+                        bail!("`;` must be at the end of a word or struct definition");
+                    }
                     continue;
                 }
             }
@@ -64,6 +147,11 @@ impl<'a> Module<'a> {
                 continue;
             }
 
+            if let Some(ref mut current_struct) = current_struct {
+                current_struct.add_field(token)?;
+                continue;
+            }
+
             // From here on out, we're at the top level in the module, which
             // means either a "meta" (import, export, or extern) or raw code
             // to be added to the `main` word if we're in the entrypoint, or
@@ -105,7 +193,7 @@ impl<'a> Module<'a> {
 
         if is_entrypoint {
             module.words.push(WordDefinition {
-                name: "main",
+                name: "main".to_string(),
                 instructions: main,
             });
         }
index e239f873ba5f099ecd07822032b22a10c2134a58..99793476d1436ddf007540d725f09fd2e65c34c2 100644 (file)
@@ -4,12 +4,20 @@ AS="${CMD_PREFIX}as"
 LD="${CMD_PREFIX}ld"
 CC="${CMD_PREFIX}cc"
 
+echo "### test 1 ###"
 ../target/debug/sorelc test1.sorel
 $AS -g -o test1.o test1.asm
 $LD -o test1.out test1.o
 ./test1.out
 
+echo "### test 2 ###"
 ../target/debug/sorelc test2.sorel
 $AS -g -o test2.o test2.asm
 $LD -o test2.out test2.o
 ./test2.out
+
+echo "### test 3 ###"
+../target/debug/sorelc test3.sorel
+$AS -g -o test3.o test3.asm
+$LD -o test3.out test3.o
+./test3.out
index 0ff5b4171a86f999e1545f79cebf1e212115d1c9..c536d066f34af54e40fd904ca9413eef54a8d9b0 100644 (file)
@@ -15,3 +15,4 @@ loop \ ( argv argc )
   1 - \ ( argv+8 argc-1 )
   dup  \ ( argv+8 argc-1 argc-1 )
 endloop 
+"\n" puts
diff --git a/tests/test3.sorel b/tests/test3.sorel
new file mode 100644 (file)
index 0000000..549c4a8
--- /dev/null
@@ -0,0 +1,29 @@
+\ vim: filetype=forth
+
+import "std:mem"
+import "std:out"
+
+:: test_struct
+  first:u8
+  second:u32
+  third:u64
+;
+
+var instance
+
+\ allocate some memory for an instance and store the pointer to it in `instance`
+sizeof.test_struct alloc instance !
+
+\ put 40, 41, and 42 into the struct at the `instance` pointer
+40 instance test_struct.first !:8
+41 instance test_struct.second !:32
+42 instance test_struct.third !
+
+\ extract the properties back onto the stack
+instance test_struct.first @:8
+instance test_struct.second @:32
+instance test_struct.third @
+
+sizeof.test_struct
+
+putstack