Files
not-python-rust/src/compiler.rs

962 lines
33 KiB
Rust
Raw Normal View History

mod visitors;
use std::collections::{HashMap, HashSet};
use std::fmt::{self, Display};
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
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
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
use std::rc::Rc;
use std::sync::LazyLock;
use assert_matches::assert_matches;
use common_macros::hash_map;
use thiserror::Error;
use crate::ast::*;
use crate::compiler::visitors::*;
use crate::obj::prelude::*;
use crate::obj::{Ptr, BUILTINS};
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
use crate::parser::Parser;
use crate::token::TokenKind;
use crate::vm::*;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
////////////////////////////////////////////////////////////////////////////////
// Misc
////////////////////////////////////////////////////////////////////////////////
fn unescape(s: &str) -> String {
s.chars()
.skip(1)
.take(s.len() - 2) // first and last chars are guaranteed to be 1 byte long
.collect::<String>()
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\t", "\t")
.replace("\\\"", "\"")
.replace("\\\'", "\'")
.replace("\\\\", "\\")
}
static BIN_OP_NAMES: LazyLock<HashMap<TokenKind, &'static str>> = LazyLock::new(|| {
hash_map! {
TokenKind::Plus => "__add__",
TokenKind::Minus => "__sub__",
TokenKind::Star => "__mul__",
TokenKind::Slash => "__div__",
TokenKind::And => "__and__",
TokenKind::Or => "__or__",
TokenKind::BangEq => "__ne__",
TokenKind::EqEq => "__eq__",
TokenKind::Greater => "__gt__",
TokenKind::GreaterEq => "__ge__",
TokenKind::Less => "__lt__",
TokenKind::LessEq => "__le__",
}
});
static UNARY_OP_NAMES: LazyLock<HashMap<TokenKind, &'static str>> = LazyLock::new(|| {
hash_map! {
TokenKind::Plus => "__pos__",
TokenKind::Minus => "__neg__",
TokenKind::Bang => "__not__",
}
});
static AUG_ASSIGN_NAMES: LazyLock<HashMap<TokenKind, &'static str>> = LazyLock::new(|| {
hash_map! {
TokenKind::PlusEq => "__add__",
TokenKind::MinusEq => "__sub__",
TokenKind::StarEq => "__mul__",
TokenKind::SlashEq => "__div__",
}
});
////////////////////////////////////////////////////////////////////////////////
// Scope
////////////////////////////////////////////////////////////////////////////////
#[derive(Debug, PartialEq)]
enum ScopeKind {
Local,
Function,
//Class,
}
#[derive(Debug)]
struct Scope {
kind: ScopeKind,
scope: Vec<Local>,
}
impl Scope {
pub fn new(kind: ScopeKind) -> Self {
Self {
kind,
scope: Default::default(),
}
}
}
////////////////////////////////////////////////////////////////////////////////
// CompileError
////////////////////////////////////////////////////////////////////////////////
#[derive(Error, Debug)]
pub enum CompileError {
Error {
line: Option<LineRange>,
source_path: String,
message: String,
},
Import {
error: Box<dyn std::error::Error>,
line: LineRange,
source_path: String,
dest_path: String,
},
}
impl Display for CompileError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
CompileError::Error {
line: Some((line, _)),
source_path,
message,
} => write!(fmt, "{source_path}: line {line}: {message}"),
CompileError::Error {
line: None,
source_path,
message,
} => write!(fmt, "{source_path}: {}", message),
CompileError::Import {
error,
line: (line, _),
source_path,
dest_path,
} => {
write!(
fmt,
"error in {dest_path} (included from {source_path} on line {line}:\n\t{error}",
)
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// Compiler
////////////////////////////////////////////////////////////////////////////////
#[derive(Debug)]
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
pub struct Compiler<'c> {
path: PathBuf,
chunks: Vec<Chunk>,
scopes: Vec<Scope>,
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
constants: &'c mut Vec<ObjP>,
imported: &'c mut HashMap<String, Ptr<Module>>,
globals: Vec<String>,
}
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
impl<'c> Compiler<'c> {
pub fn new(
path: PathBuf,
constants: &'c mut Vec<ObjP>,
imported: &'c mut HashMap<String, Ptr<Module>>,
) -> Self {
Compiler {
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
path,
chunks: Default::default(),
scopes: Default::default(),
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
constants,
imported,
globals: BUILTINS
.with_borrow(|builtins| builtins.keys().map(ToString::to_string).collect()),
}
}
fn chunk(&self) -> &Chunk {
self.chunks.last().expect("no chunk")
}
fn chunk_mut(&mut self) -> &mut Chunk {
self.chunks.last_mut().expect("no chunk")
}
fn scope(&self) -> &Scope {
self.scopes.last().expect("no scope")
}
fn scope_mut(&mut self) -> &mut Scope {
self.scopes.last_mut().expect("no scope")
}
fn is_global_scope(&self) -> bool {
self.scopes.is_empty()
}
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
pub fn compile_path(self, path: impl AsRef<Path>) -> Result<Ptr<Module>> {
let path_str = &path.as_ref().as_os_str().to_str().unwrap();
let mut file = File::open(path.as_ref()).map_err(|e| CompileError::Error {
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
line: None,
source_path: self.path.display().to_string(),
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
message: format!("could not open {}: {}", path.as_ref().display(), e),
})?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let mut parser = Parser::new(contents, &path_str)?;
let ast = parser.parse_all()?;
if parser.was_error() {
return Err(CompileError::Error {
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
line: None,
source_path: self.path.display().to_string(),
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
message: format!("error in '{}'", path.as_ref().display()),
}
.into());
}
self.compile(&path_str, &ast)
}
/// Compiles a body of code.
///
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
/// This returns the module of the compiled program.
pub fn compile(mut self, path: impl ToString, body: &Vec<StmtP>) -> Result<Ptr<Module>> {
self.chunks.push(Chunk::default());
for stmt in body {
self.compile_stmt(stmt)?;
}
// add halt instruction with last line, if any
let mut last_line = (0, 0);
if let Some(last) = body.last() {
last_line = stmt_line_number(last.as_ref());
}
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
self.emit(last_line, Op::ExitModule);
let chunk = self.chunks.pop().expect("no chunk");
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
// This is allowed because obviously it is a pointer to a Module. We can upcast later.
let module = Module::create(path.to_string(), Rc::new(chunk), self.globals);
let module = unsafe {
let ptr = Ptr::into_raw(module) as *const gc::GcCell<Module>;
Ptr::from_raw(ptr)
};
Ok(module)
}
fn compile_stmt(&mut self, stmt: &StmtP) -> Result<()> {
stmt.accept(self)
}
fn compile_expr(&mut self, expr: &ExprP) -> Result<()> {
expr.accept(self)
}
fn insert_constant(&mut self, constant: ObjP) -> Result<ConstantId> {
// simple interning - try to find a constant that is exactly equal to this one and just
// return its value instead
for (index, interned) in self.constants.iter().enumerate() {
// check if the two objects are the same type. If they are not, this can cause an
// interesting bug where two objects that are different types are considered equal. The
// best example is a Float(1.0) and Int(1) are considered equal.
if constant
.borrow()
.ty()
.borrow()
.equals(&*interned.borrow().ty().borrow())
&& constant.borrow().equals(&*interned.borrow())
{
return Ok(index as ConstantId);
}
}
let index = self.constants.len();
if index > (ConstantId::MAX as usize) {
return Err(CompileError::Error {
line: None,
source_path: self.path.display().to_string(),
message: format!("too many constants (maximum {})", ConstantId::MAX),
}
.into());
}
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
// convert this to a pointer, upcast, and then re-GC
self.constants.push(constant);
Ok(index as ConstantId)
}
fn get_global(&self, name: &str) -> Option<GlobalId> {
self.globals
.iter()
.position(|global| global == &name)
.map(|id| id as GlobalId)
}
fn insert_global(&mut self, name: &str) -> Result<GlobalId> {
if let Some(id) = self.get_global(name) {
return Ok(id);
}
let index = self.globals.len();
if index > (GlobalId::MAX as usize) {
return Err(CompileError::Error {
line: None,
source_path: self.path.display().to_string(),
message: format!("too many globals (maximum {})", GlobalId::MAX),
}
.into());
}
self.globals.push(name.to_string());
Ok(index as GlobalId)
}
/// Get a nonlocal binding to a variable.
///
/// This will return how many stack frames up we should look for this nonlocal, the `Local`
/// that defines this binding.
fn get_nonlocal(&self, name: &str) -> Option<(FrameDepth, &Local)> {
let mut is_local = true;
let mut depth = 0;
for scope in self.scopes.iter().rev() {
if scope.kind == ScopeKind::Function {
// no longer inside the local scope
if is_local {
is_local = false;
continue;
}
// increase stack frame search
depth += 1;
}
// skip local variables
if is_local {
continue;
}
// outside of the local scope, check if we hvae defined the sought-after name
for local in &scope.scope {
if local.name == name {
return Some((depth, local));
}
}
}
None
}
fn get_local(&self, name: &str) -> Option<&Local> {
for scope in self.scopes.iter().rev() {
for local in &scope.scope {
if local.name == name {
return Some(local);
}
}
if scope.kind == ScopeKind::Function {
break;
}
}
None
}
fn insert_local(&mut self, name: String) -> Result<&Local> {
let index = self.chunk().locals.len();
if index > (LocalIndex::MAX as usize) {
return Err(CompileError::Error {
line: None,
source_path: self.path.display().to_string(),
message: format!("too many locals (maximum: {})", LocalIndex::MAX),
}
.into());
}
let mut local = Local {
slot: 0,
index: index as LocalIndex,
name,
};
// get the last allocated slot
for scope in self.scopes.iter().rev() {
if scope.scope.len() == 0 {
if scope.kind == ScopeKind::Function {
// don't go above the current function's scope (which was just determined to be
// empty)
break;
}
continue;
}
// get the last allocated slot and increment by one
let last = &scope.scope.last().unwrap();
if last.slot == LocalSlot::MAX {
return Err(CompileError::Error {
line: None,
source_path: self.path.display().to_string(),
message: format!(
"too many stack slots used by locals(maximum: {})",
LocalSlot::MAX
),
}
.into());
}
local.slot = last.slot + 1;
break;
}
self.scope_mut().scope.push(local.clone());
self.chunk_mut().locals.push(local);
Ok(self.scope().scope.last().unwrap())
}
fn begin_scope(&mut self, kind: ScopeKind) {
self.scopes.push(Scope::new(kind));
}
fn end_scope(&mut self, line: LineRange) {
let scope = self.scopes.pop().expect("no scope");
for _local in scope.scope {
self.emit(line, Op::Pop);
}
}
fn emit(&mut self, line: LineRange, op: Op) {
let chunk = self.chunk_mut();
chunk.code.push(op);
chunk.lines.push(line);
}
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
/// Emit an assign statement based on the current scope - i.e. `Op::SetGlobal` if we're
/// in global scope, or `Op::SetLocal` if we're in a function or local scope.
fn emit_assign(&mut self, line: LineRange, name: &str) -> Result<()> {
if self.is_global_scope() {
let global = self.insert_global(name)?;
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
self.emit(line, Op::SetGlobal(global));
} else {
let mut declare = false;
let local = if let Some(local) = self.get_local(name) {
local
} else {
declare = true;
self.insert_local(name.to_string())?
}
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
.clone();
if !declare {
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
self.emit(line, Op::SetLocal(local.index));
}
}
Ok(())
}
/// Emit a name lookup. This will either emit `Op::GetLocal` or `Op::GetGlobal`, depending on
/// the context. If the local or global is not found in the current scope, we error out
/// instead.
fn emit_lookup(&mut self, line: LineRange, name: &str) -> Result<()> {
// check if there's a local with this name, otherwise check globals
if let Some(local) = self.get_local(name) {
self.emit(line, Op::GetLocal(local.index));
} else {
let global = self.get_global(name).ok_or_else(|| CompileError::Error {
line: Some(line),
source_path: self.path.display().to_string(),
message: if self.is_global_scope() {
format!("unknown global {}", name)
} else {
format!("unknown local {}", name)
},
})?;
self.emit(line, Op::GetGlobal(global));
}
Ok(())
}
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
fn search_dir(&self) -> &Path {
self.path.parent().unwrap()
}
}
impl StmtVisitor for Compiler<'_> {
fn visit_import_stmt(&mut self, stmt: &ImportStmt) -> Result<()> {
const EXT: &str = "npp";
let line = stmt_line_number(stmt);
// allocate names - local or global
let nil_constant = self.insert_constant(Nil::create())?;
for what in &stmt.what {
if self.is_global_scope() {
self.insert_global(&what.text)?;
} else {
self.emit((what.line, what.line), Op::PushConstant(nil_constant));
self.insert_local(what.text.to_string())?;
}
}
if stmt.what.is_empty() && stmt.module.kind == TokenKind::Name {
if self.is_global_scope() {
self.insert_global(&stmt.module.text)?;
} else {
self.emit(
(stmt.module.line, stmt.module.line),
Op::PushConstant(nil_constant),
);
self.insert_local(stmt.module.text.to_string())?;
}
}
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
// resolve filename and get full filepath
let path = match stmt.module.kind {
TokenKind::Name => self
.search_dir()
.join(format!("{}.{EXT}", stmt.module.text)),
TokenKind::String => {
let path = PathBuf::from(unescape(&stmt.module.text));
if path.is_absolute() {
path
} else {
std::path::absolute(self.search_dir().join(path))?
}
}
_ => unreachable!(),
};
// check if this has already been registered with our compile session and just use that if
// so
let path_str = path.as_os_str().to_str().unwrap();
let module = if let Some(imported) = self.imported.get(path_str) {
// use the imported module
imported.clone()
} else {
// otherwise compile and create a new Module object and insert it as a constant and
// also into the modules cache
let module = Compiler::new(path.clone(), self.constants, self.imported)
.compile_path(&path)
.map_err(|error| CompileError::Import {
error,
line,
source_path: self.path.display().to_string(),
dest_path: path_str.to_string(),
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
})?;
self.imported.insert(path_str.to_string(), module.clone());
module
};
let module_constant = self.insert_constant(upcast_obj(module.clone()))?;
// evaluate module
self.emit(line, Op::PushConstant(module_constant));
self.emit(line, Op::EvalModule);
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
if stmt.what.is_empty() {
// assign the resulting object to the module name as appropriate
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
// only assign if it's a name, if it's a string we don't assign anything
if stmt.module.kind == TokenKind::Name {
self.emit_assign(line, &stmt.module.text)?;
} else {
self.emit(line, Op::Pop);
}
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
} else {
// evaluate the module, and then assign all names that were imported as appropriate
for what in stmt.what.iter() {
self.emit(line, Op::Dup);
let constant_id = self.insert_constant(Str::create(&what.text))?;
self.emit(line, Op::GetAttr(constant_id));
self.emit_assign(line, &what.text)?;
}
// not importing the module name itself, so clean up the stack after we're done setting
// names
self.emit(line, Op::Pop);
}
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
Ok(())
}
fn visit_expr_stmt(&mut self, stmt: &ExprStmt) -> Result<()> {
self.compile_expr(&stmt.expr)?;
self.emit(stmt_line_number(stmt), Op::Pop);
Ok(())
}
fn visit_assign_stmt(&mut self, stmt: &AssignStmt) -> Result<()> {
let name = &stmt.lhs.text;
let line = stmt_line_number(stmt);
match stmt.op.kind {
// normal assignment
TokenKind::Eq => {
// compile RHS
self.compile_expr(&stmt.rhs)?;
// If the last value that was assigned to is a function, set its name here
// TODO - maybe this would be smarter to set up in the AST. I'm 99% sure that the last
// object created, if it were a function object, will be what we're assigning it to, but I
// want to be 100% sure instead of 99%.
if let Some(obj) = self.constants.last() {
if let Some(fun) = obj.borrow_mut().as_any_mut().downcast_mut::<UserFunction>()
{
fun.set_name(Rc::new(name.to_string()));
}
}
// compile LHS
self.emit_assign(line, name)?;
}
// augmented assignment
TokenKind::PlusEq | TokenKind::MinusEq | TokenKind::StarEq | TokenKind::SlashEq => {
self.emit_lookup(line, name)?;
let op_name = AUG_ASSIGN_NAMES
.get(&stmt.op.kind)
.expect("invalid augmented assignment operator");
let op_constant = self.insert_constant(Str::create(op_name))?;
self.emit(line, Op::GetAttr(op_constant));
self.compile_expr(&stmt.rhs)?;
self.emit(line, Op::Call(1));
self.emit_assign(line, name)?;
}
_ => unreachable!(),
}
Ok(())
}
fn visit_set_stmt(&mut self, stmt: &SetStmt) -> Result<()> {
let name = self.insert_constant(Str::create(&stmt.name.text))?;
let line = stmt_line_number(stmt);
self.compile_expr(&stmt.expr)?;
match stmt.op.kind {
// normal assignment
TokenKind::Eq => {
self.compile_expr(&stmt.rhs)?;
self.emit(line, Op::SetAttr(name));
}
// augmented assignment
TokenKind::PlusEq | TokenKind::MinusEq | TokenKind::StarEq | TokenKind::SlashEq => {
//
self.emit(line, Op::Dup);
self.emit(line, Op::GetAttr(name));
let op = AUG_ASSIGN_NAMES
.get(&stmt.op.kind)
.expect("invalid augmented assign operator");
let op_constant = self.insert_constant(Str::create(op))?;
self.emit(line, Op::GetAttr(op_constant));
self.compile_expr(&stmt.rhs)?;
self.emit(line, Op::Call(1));
self.emit(line, Op::SetAttr(name));
}
_ => unreachable!(),
}
self.compile_expr(&stmt.rhs)?;
Ok(())
}
fn visit_block_stmt(&mut self, stmt: &BlockStmt) -> Result<()> {
self.begin_scope(ScopeKind::Local);
for s in &stmt.stmts {
self.compile_stmt(s)?;
}
self.end_scope((stmt.rbrace.line, stmt.rbrace.line));
Ok(())
}
fn visit_return_stmt(&mut self, stmt: &ReturnStmt) -> Result<()> {
if let Some(expr) = &stmt.expr {
self.compile_expr(expr)?;
} else {
let nil = self.insert_constant(Nil::create())?;
self.emit(stmt_line_number(stmt), Op::PushConstant(nil));
}
self.emit(stmt_line_number(stmt), Op::Return);
Ok(())
}
fn visit_if_stmt(&mut self, stmt: &IfStmt) -> Result<()> {
// condition
self.compile_expr(&stmt.condition)?;
// call obj.to_bool()
let bool_attr = self.insert_constant(Str::create("to_bool"))?;
self.emit(expr_line_number(&*stmt.condition), Op::GetAttr(bool_attr));
self.emit(expr_line_number(&*stmt.condition), Op::Call(0));
let condition_patch_index = self.chunk().code.len();
self.emit(expr_line_number(&*stmt.condition), Op::JumpFalse(0));
// then branch
// pop the condition on top of the stack (no jump taken)
self.emit(expr_line_number(&*stmt.condition), Op::Pop);
// not using compile_stmt because then_branch isn't a pointer, it's an honest-to-goodness
// value
stmt.then_branch.accept(self)?;
let exit_patch_index = self.chunk().code.len();
self.emit(stmt_line_number(&stmt.then_branch), Op::Jump(0));
// else branch
// patch the condition index - this is where the JUMP_FALSE will jump to
assert_matches!(self.chunk().code[condition_patch_index], Op::JumpFalse(_));
let offset = self.chunk().code.len() - condition_patch_index;
assert!(
offset <= (JumpOpArg::MAX as usize),
"jump offset too large between lines {:?} - this is a compiler limitation, sorry",
stmt_line_number(&stmt.then_branch)
);
self.chunk_mut().code[condition_patch_index] = Op::JumpFalse(offset as JumpOpArg);
// pop the condition on top of the stack (jump taken)
self.emit(expr_line_number(&*stmt.condition), Op::Pop);
for s in &stmt.else_branch {
self.compile_stmt(s)?;
}
// patch the "then" branch exit jump address - this is where Op::Jump will jump to.
// TODO : see if we can eliminate duplicates by checking the last two instructions
assert_matches!(self.chunk().code[exit_patch_index], Op::Jump(_));
let offset = self.chunk().code.len() - condition_patch_index;
assert!(
offset <= (JumpOpArg::MAX as usize),
"jump offset too large between lines {:?} - this is a compiler limitation, sorry",
stmt_line_number(&stmt.then_branch)
);
self.chunk_mut().code[exit_patch_index] = Op::Jump(offset as JumpOpArg);
Ok(())
}
}
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
impl ExprVisitor for Compiler<'_> {
fn visit_binary_expr(&mut self, expr: &BinaryExpr) -> Result<()> {
self.compile_expr(&expr.lhs)?;
// short-circuit setup
let mut exit_patch_index = 0;
if let TokenKind::And | TokenKind::Or = expr.op.kind {
let constant_id = self.insert_constant(Str::create("to_bool"))?;
self.emit(expr_line_number(&*expr.lhs), Op::GetAttr(constant_id));
self.emit(expr_line_number(&*expr.lhs), Op::Call(0));
exit_patch_index = self.chunk().code.len();
if expr.op.kind == TokenKind::And {
self.emit((expr.op.line, expr.op.line), Op::JumpFalse(0));
} else {
self.emit((expr.op.line, expr.op.line), Op::JumpTrue(0));
}
}
let name = BIN_OP_NAMES
.get(&expr.op.kind)
.expect("invalid binary operator");
let constant_id = self.insert_constant(Str::create(name))?;
self.emit(expr_line_number(expr), Op::GetAttr(constant_id));
self.compile_expr(&expr.rhs)?;
// convert RHS to a bool if we're doing AND or OR
if let TokenKind::And | TokenKind::Or = expr.op.kind {
let constant_id = self.insert_constant(Str::create("to_bool"))?;
self.emit(expr_line_number(&*expr.rhs), Op::GetAttr(constant_id));
self.emit(expr_line_number(&*expr.rhs), Op::Call(0));
}
// call operator function
self.emit(expr_line_number(expr), Op::Call(1));
// patch exit if we're doing a short circuit
if exit_patch_index != 0 {
assert_matches!(
self.chunk().code[exit_patch_index],
Op::JumpTrue(_) | Op::JumpFalse(_)
);
let offset = self.chunk().code.len() - exit_patch_index;
// don't worry about doing a check on if offset is small enough for JumpOpArg, if you
// have 4 billion instructions between jumps that is probably your own fault
let new_op = match self.chunk().code[exit_patch_index] {
Op::JumpTrue(_) => Op::JumpTrue(offset as JumpOpArg),
Op::JumpFalse(_) => Op::JumpFalse(offset as JumpOpArg),
_ => unreachable!(),
};
self.chunk_mut().code[exit_patch_index] = new_op;
}
Ok(())
}
fn visit_unary_expr(&mut self, expr: &UnaryExpr) -> Result<()> {
self.compile_expr(&expr.expr)?;
let name = UNARY_OP_NAMES
.get(&expr.op.kind)
.expect("invalid unary operator");
let constant_id = self.insert_constant(Str::create(name))?;
self.emit(expr_line_number(expr), Op::GetAttr(constant_id));
self.emit(expr_line_number(expr), Op::Call(0));
Ok(())
}
fn visit_call_expr(&mut self, expr: &CallExpr) -> Result<()> {
self.compile_expr(&expr.expr)?;
for arg in &expr.args {
self.compile_expr(arg)?;
}
if expr.args.len() > (Argc::MAX as usize) {
return Err(CompileError::Error {
line: Some(expr_line_number(expr)),
source_path: self.path.display().to_string(),
message: format!("too many function arguments (maximum: {})", Argc::MAX),
}
.into());
}
self.emit(expr_line_number(expr), Op::Call(expr.args.len() as Argc));
Ok(())
}
fn visit_get_expr(&mut self, expr: &GetExpr) -> Result<()> {
self.compile_expr(&expr.expr)?;
let constant_id = self.insert_constant(Str::create(&expr.name.text))?;
self.emit(expr_line_number(expr), Op::GetAttr(constant_id));
Ok(())
}
fn visit_index_expr(&mut self, expr: &IndexExpr) -> Result<()> {
self.compile_expr(&expr.expr)?;
let constant_id = self.insert_constant(Str::create("__index__"))?;
self.emit(expr_line_number(expr), Op::GetAttr(constant_id));
self.compile_expr(&expr.index)?;
self.emit(expr_line_number(expr), Op::Call(1));
Ok(())
}
fn visit_primary_expr(&mut self, expr: &PrimaryExpr) -> Result<()> {
match expr.token.kind {
TokenKind::Name => {
let name = &expr.token.text;
let line = expr_line_number(expr);
self.emit_lookup(line, name)?;
}
TokenKind::Number => {
let obj = if expr.token.text.contains('.') {
Float::create(expr.token.text.parse().unwrap())
} else if expr.token.text.starts_with("0x") || expr.token.text.starts_with("0X") {
Int::create(i64::from_str_radix(&expr.token.text[2..], 16).unwrap())
} else if expr.token.text.starts_with("0b") || expr.token.text.starts_with("0B") {
Int::create(i64::from_str_radix(&expr.token.text[2..], 2).unwrap())
} else {
Int::create(expr.token.text.parse().unwrap())
};
let constant_id = self.insert_constant(obj)?;
self.emit(expr_line_number(expr), Op::PushConstant(constant_id));
}
TokenKind::String => {
let constant_id = self.insert_constant(Str::create(unescape(&expr.token.text)))?;
self.emit(expr_line_number(expr), Op::PushConstant(constant_id));
}
TokenKind::True | TokenKind::False => {
let constant_id =
self.insert_constant(Bool::create(expr.token.kind == TokenKind::True))?;
self.emit(expr_line_number(expr), Op::PushConstant(constant_id));
}
TokenKind::Nil => {
let constant_id = self.insert_constant(Nil::create())?;
self.emit(expr_line_number(expr), Op::PushConstant(constant_id));
}
_ => unreachable!(),
}
Ok(())
}
fn visit_function_expr(&mut self, expr: &FunctionExpr) -> Result<()> {
let end_line = (expr.rbrace.line, expr.rbrace.line);
self.begin_scope(ScopeKind::Function);
self.chunks.push(Chunk::default());
let mut locals: HashSet<String> = Default::default();
for (param, _ty) in &expr.params {
// register all params as locals
locals.insert(param.text.to_string());
// also insert them as locals in the scope
self.insert_local(param.text.to_string())?;
}
// closures: figure out all other locals that are assigned to in the function
for local in LocalAssignCollector::collect(&expr.body) {
locals.insert(local);
}
// figure out all nonlocals being used, and then re-register them as locals
// when a user function is called, all values of the nonlocal are pushed to the top of the
// stack on top of the function parameters.
let all_names = LocalNameCollector::collect(&expr.body);
// these are the nonlocals that we're copying/re-registering as locals
let mut captures: HashMap<String, Local> = Default::default();
let mut nonlocals: HashMap<String, (FrameDepth, Local)> = Default::default();
for name in &all_names {
// already registered as a local
if locals.contains(name) {
continue;
}
// already captured
if captures.contains_key(name) {
continue;
}
if let Some((depth, nonlocal)) = self.get_nonlocal(name) {
let nonlocal = nonlocal.clone();
nonlocals.insert(name.to_string(), (depth, nonlocal));
captures.insert(
name.to_string(),
self.insert_local(name.to_string())?.clone(),
);
}
}
// compile body
for stmt in &expr.body {
self.compile_stmt(stmt)?;
}
// always end with a "return nil"
let nil = self.insert_constant(Nil::create())?;
self.emit(end_line, Op::PushConstant(nil));
self.emit(end_line, Op::Return);
self.end_scope(end_line);
// create the function
let chunk = self.chunks.pop().unwrap();
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
let fun = UserFunction::create(
&self.path.as_os_str().to_str().unwrap(),
chunk,
expr.params.len() as Argc,
);
// register the function as a constant
let fun_constant = self.insert_constant(fun)?;
self.emit(expr_line_number(expr), Op::PushConstant(fun_constant));
// close over the captured values
for (depth, local) in nonlocals.values() {
self.emit(
expr_line_number(expr),
Op::CloseOver {
depth: *depth,
slot: local.slot,
},
);
}
Ok(())
}
fn visit_list_expr(&mut self, expr: &ListExpr) -> Result<()> {
let line = expr_line_number(expr);
for expr in &expr.exprs {
self.compile_expr(expr)?;
}
self.emit(line, Op::BuildList(expr.exprs.len() as ListLen));
Ok(())
}
}