Commit Graph

21 Commits

Author SHA1 Message Date
16ab9d718b Add augmented assignment operators for set statements
This allows us to do things like

    self.foo += 5

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-07 11:05:39 -07:00
365bee0554 Implement augmented assignment operators
Add support for +=, -=, *=, and /= operators. This is basically just
syntactic sugar, but it's still nice to have

    a += 1

compiles to the equivalent of

    a = a + 1

with all the same implications of scoping rules.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-07 10:23:15 -07:00
8179611c23 Add slightly more readable error messages in the compiler
Previously, the CompileError error messages were just `Debug::fmt`
written to stdout and there wasn't really a backtrace in the code
included. Now, when there is an error in an imported file, it will
display a backtrace of the files included that caused this error.

These are not perfect error messages and are a bit rough around the
edges but they are good enough for now.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-06 20:26:14 -07:00
b21a02f12f Move utility visitors out of compiler.rs
We are converting a 1200 line file into an 800 and 400 line files. It's
actually a lot easier to read now, those visitors rarely ever change and
they get in the way of me reading the file (with my eyes, not with a
program).

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-04 20:26:18 -07:00
17408a8695 Implement import a, b, c from foo syntax
This brings stuff into the local scope, but it is a little funky with
local scopes that are above the current level (in the same function or
module).

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-04 20:12:21 -07:00
f0de5f7850 WIP: Add imports and modules
This is a big change because it touches a lot of stuff, but here is the
overview:

* Import syntax:
    ```
    import foo
    import bar from foo
    import bar from "foo.npp"
    import bar, baz from foo
    import * from foo
    import "foo.npp"
    ```
    * These are all valid imports. They should be pretty
      straightforward, maybe with exception of the last item. If you are
      importing a path directly, but not importing any members from it,
      it does not insert anything into the current namespace, and just
      executes the file. This is probably going to be unused but I want
      to include it for completeness. We can always remove it later
      before a hypothetical 1.0 release.
    * The "from" keyword is only ever used as a keyword here, and I am
      allowing it to be used as an identifier elsewhere. Don't export
      it, because that's weird and wrong and won't work.
* Modules:
    * Doing an `import foo` will look for "foo.npp" at compile-time,
      relative to the importer's directory, parse it, and compile it.
      The importer will then attempt to execute the module with the new
      `EnterModule` op. This instruction will execute the module kind of
      like a function, assigning the module's global namespace to an
      object that you can pass around.
    * `import bar from foo` and `import bar from "foo.npp"` et al syntax
      is not currently implemented in the compiler.
    * There is a new "Module" object that represents a potentially
      un-initialized module. This can't be referred to directly in code.
* VM:
    * The VM operates around Module objects now. If you want to "call" a
      new module, you should call `enter_module`. This is how the main
      chunk is invoked.
* TODOs:
    * `exit_module` function in the VM
    * Finish up module implementation in compiler
    * Built-in modules
    * Sub-modules - e.g. `import foo.bar` - how does naming work for
      this?
    * Module directories. In Python you have `foo/__init__.py` and in
      Rust you have `foo/mod.rs`.
    * Probably a "Namespace" object that explicitly denotes "this is an
      imported module that you're dealing with"
    * Tests, tests, tests

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-04 10:11:49 -07:00
e5756d6f1a Don't always assume that an assignment statement will result in a constant
If an assignment statement has a list as its RHS, it will not create a
constant. We now do a conditional lookup of the last constant index
instead.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-30 17:39:41 -07:00
9ec12774fd Check type equality when inserting a constant
When the compiler inserts a constant, it will first check to see if that
constant already has been created, so we aren't making millions of the
same constant value - e.g. we can reuse the same integer.

However, the .equals() function on all Object values was returning a
false positive against Ints and Floats that have the same numeric value,
i.e. Float(1.0) == Int(1). If, for example, a float 1.0 was inserted as
a constant, and then an integer 1 was used as a constant later, it was
erroneously retrieving the float 1.0 as an interned pointer value.

This is fixed by checking if the two values' types are equal as well.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-30 17:33:43 -07:00
dab474a037 Add lists
This introduces:

* new syntax for list literals, put comma-separated values between
  braces for your new list
* new syntax for indexing, do `foo[index]` to get the value in `foo` at
  `index`. Lists also allow negative indices too. Any type that wants to
  be indexed can include their own __index__ function as well.
* new VM instruction, BuildList. List literals were a lot easier to
  implement using this rather than creating a new list, creating a
  temporary stack value, and then duplicating + pushing to that
  temporary value over and over.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-30 16:33:58 -07:00
43183d6553 Most object types get their own file now
This is hopefully going to make navigating the source tree easier.
Hopefully.

The only types that don't get their own files are:

* function types (UserFunction, BuiltinFunction, Method), which all live
  in obj/function.rs
* Nil, which lives in obj.rs
* Obj, which lives in obj.rs

Type definitions and init_types now live in obj/ty.rs.

New obj::prelude module for common imports.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-30 15:15:41 -07:00
9d5d094c5b Big Object naming refactor
* trait Obj -> Object
* Remove *Inst suffix from all object types. ObjInst -> Obj, IntInst ->
  Int, etc
* Type -> Ty, type_inst() -> ty(), type_name() -> ty_name()

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-26 11:07:12 -07:00
1dd058ae18 Add binary and hex number parsing
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-26 10:03:54 -07:00
2203957ebb Typesystem global instance churn, again
I don't know if I'm ever going to get this right.

It's a massive pain having to pass around the base "Method" type
everywhere. It really makes a lot more sense to have it already defined
someplace statically available. It makes doing like getting an attribute
or vtable entry a lot more ergonomic. Previously we'd have to pass in
the Method type every time, which was silly. Now we can just let the
MethodInst::instantiate() function query it directly. Like, this is
100000% better.

Also, I got rid of get_attr_lazy in favor of get_vtable_attr. I think
that I want to unify get_attr and get_vtable_attr, but that would
require a GC pointer to the "self" object on every object that you
create. That's a bit iffy.

But for now, things are feeling a little better and all the tests are
passing, so that's good at least.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-25 10:22:03 -07:00
890467e02c Compiler emits return instructions
Another failure on my part to write the compiler correctly. oops

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 16:54:36 -07:00
3e769e9c48 Compile RHS of binary expressions (oopsie)
Not sure how this happened besides being a gigantic moron, I completely
forgot to do `self.compile_expr(&expr.rhs)`.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 12:27:53 -07:00
078aef70ea Split up src/obj.rs
* common macros are in their own private module
* functions are in their own obj::function module

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 09:03:34 -07:00
3545488ef8 Add TODO notice on a sorta-important scoping bug
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 08:37:29 -07:00
9c4898ff8d Add base object function stubs(!)
Big step here, we have function stubs available for everybody. Most of
them panic. Each type will eventually have its own implementations for
different operators.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 21:34:10 -07:00
8b931e9d12 Revamp object system, start using gc crate
Wow, what a ride. I think everything should be working now. In short:

* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
  type. I may choose to implement my own in contiguous memory in the
  future. We will see.
* The type system is no longer global. This is a bit of a burden,
  because now, whenever you want to create a new object, you need to
  pass its type object into the `Obj::instantiate` method, as well as
  its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
  helps create new objects as long as you have access to a `builtins`
  hashmap. So something that would normally look like this:

    fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
        let print_builtin = upcast_obj(BuiltinFunctionInst::create(
            ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
            "print",
            print,
            1
        );
        builtins.insert("print".to_string(), print_builtin)
        // other builtins inserted here...
    }

  now looks like this:

    fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
        let print_builtin = builtins.create_builtin_function("print", print, 1);
        builtins.insert("print".to_string(), print_builtin);
    }

(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))

Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.

Next up is vtables.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
24b06851c7 WIP: move mutability to be internal to the object instead of the pointer
I'm not super happy with this. But, the RwLock has been moved to the
`BaseObjInst::attrs` member. Although this is not exactly how it appears
in code, it basically does this:

    type Ptr<T> = Arc<RwLock<T>>;

    struct BaseObjInst {
        attr: HashMap<String, Ptr<dyn Obj>>,
        // etc
    }

becomes

    type Ptr<T> = Arc<T>;

    struct BaseObjInst {
        attr: RwLock<HashMap<String, ObjP>>,
        // etc
    }

This makes things a lot more ergonomic (don't have to use try_read() and
try_write() everywhere), but it also eliminates compile-time errors that
would catch mutability errors. This is currently rearing its ugly head
when initializing the typesystem, since `Type` needs to hold a circular
reference itself (which it already shouldn't be doing since it's a
reference-counted pointer!). Currently, all tests are failing because of
this limitation.

There are a couple of ways around this limitation.

The first solution would be just copying  all of the object
instantiation code into the `init_types` function and avoid calling
`some_base_type.instantiate()`. This would probably be literal
copy-pasting, or maybe an (ugly) macro, and probably a nightmare to
maintain long-term. I don't like this option, but it would make
everything "just work" with reference-counted pointers.

The second solution would be to write our own garbage collector, which
would allow for circular references and (hypothetically) mutably
updating these references. This is something that I am looking into,
because I really want a RefCell that you can pass around in a more
ergonomic way.

I think the fundamental error that I'm running into is trying to borrow
the same value multiple times mutably, which you *really* shouldn't be
doing. I believe I need to write better code and does the same thing.

The only unsolved problem is circular references. This is not a problem
right now because I'm not writing code that has circular references
besides the base typesystem (which is not a problem because they need to
live the entire lifetime of the program), but it will be a latent
problem until it gets fixed.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-22 20:40:15 -07:00
16f3dc960c Base initial commit
Still WIP, working on object system still, which in Rust, makes me want
to kill myself

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-20 16:04:30 -07:00