Update how scope rules work, and update implementation
* Currently, scopes are only allowed to look at their locals and the
globals. Inner functions cannot refer to values in their parent
functions. This will change eventually.
* Scope lookup is split between globals and locals. Locals are defined
in a scope if they are explicitly assigned to.
* i.e. `a = foo` will treat `a` as a local in the current scope if
it appears anywhere in that scope. This does not extend to
setattrs; `a.b = foo` will not trigger `a` into being a local var.
* `Package` objects are no longer returned from the compiler - instead,
a user function is returned.
* Other various changes and renames
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
pub mod basic_block;
|
||||
pub mod error;
|
||||
mod locals;
|
||||
mod scope;
|
||||
pub mod thunk;
|
||||
|
||||
use crate::{syn::ast::Body, obj::prelude::*, vm::{consts::*, package::Package}};
|
||||
use crate::{syn::ast::Body, obj::prelude::*, vm::consts::*};
|
||||
use scope::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Compile {
|
||||
const_data: ConstData,
|
||||
globals: Locals,
|
||||
scope: Vec<Locals>,
|
||||
names: Vec<Sym>,
|
||||
next_local: Name,
|
||||
scope: Scope,
|
||||
}
|
||||
|
||||
impl Compile {
|
||||
@@ -22,12 +21,24 @@ impl Compile {
|
||||
}
|
||||
|
||||
/// Compiles the given AST body.
|
||||
pub fn compile(mut self, body: &Body) -> error::Result<Package> {
|
||||
pub fn compile(mut self, body: &Body) -> error::Result<(ConstPool, UserFunRef)> {
|
||||
self.push_scope_layer();
|
||||
let main = thunk::CompileBody::new(&mut self)
|
||||
.compile(body)?
|
||||
.flatten()
|
||||
.to_vec();
|
||||
Ok(Package::new(self.names, self.const_data.const_pool, main))
|
||||
let globals_syms: std::collections::BTreeMap<_, _> = self.pop_scope_layer().unwrap()
|
||||
.into_iter()
|
||||
.map(|(sym, name)| (name, sym))
|
||||
.collect();
|
||||
let globals = globals_syms.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (name, sym))| {
|
||||
assert_eq!(index, name.index());
|
||||
sym
|
||||
})
|
||||
.collect();
|
||||
Ok((self.const_data.const_pool, UserFun::new_obj(main, globals)))
|
||||
}
|
||||
|
||||
/// Gets the constant data that is interned in this compile session.
|
||||
@@ -55,64 +66,34 @@ impl Compile {
|
||||
self.const_data_mut().push(obj)
|
||||
}
|
||||
|
||||
/// Looks up a variable name.
|
||||
pub fn lookup_scope(&mut self, sym: Sym) -> Option<Name> {
|
||||
self.scope
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|locals| locals.get(&sym))
|
||||
.next()
|
||||
.or_else(|| self.globals.get(&sym))
|
||||
.copied()
|
||||
/// Looks up a local variable name.
|
||||
pub fn lookup_local(&mut self, sym: Sym) -> Option<Name> {
|
||||
self.scope.lookup_local(sym)
|
||||
}
|
||||
|
||||
/// Looks up a variable name, or creates a global name if it doesn't exist.
|
||||
pub fn lookup_scope_or_create_global(&mut self, sym: Sym) -> Name {
|
||||
if let Some(local) = self.lookup_scope(sym) {
|
||||
local
|
||||
} else {
|
||||
self.create_global(sym)
|
||||
}
|
||||
/// Looks up a global variable name.
|
||||
pub fn lookup_global(&mut self, sym: Sym) -> Option<Name> {
|
||||
self.scope.lookup_global(sym)
|
||||
}
|
||||
|
||||
/// Creates a new local variable if it does not exist in the current local scope.
|
||||
pub(crate) fn create_local(&mut self, sym: Sym) -> Name {
|
||||
let locals = self.scope.last_mut().expect("scope");
|
||||
if let Some(local) = locals.get(&sym) {
|
||||
*local
|
||||
} else {
|
||||
// wish I could use mem::replace here, oh well
|
||||
let local = self.next_local;
|
||||
self.next_local = self.next_local.next();
|
||||
locals.insert(sym, local);
|
||||
self.names.push(sym);
|
||||
assert_eq!(self.names.len(), self.next_local.index());
|
||||
local
|
||||
}
|
||||
self.scope.create_local(sym)
|
||||
}
|
||||
|
||||
/// Creates a new global variable if it does not exist in the current global scope.
|
||||
pub fn create_global(&mut self, sym: Sym) -> Name {
|
||||
if let Some(global) = self.globals.get(&sym) {
|
||||
*global
|
||||
} else {
|
||||
let global = self.next_local;
|
||||
self.next_local = self.next_local.next();
|
||||
self.globals.insert(sym, global);
|
||||
self.names.push(sym);
|
||||
assert_eq!(self.names.len(), self.next_local.index());
|
||||
global
|
||||
}
|
||||
self.scope.create_global(sym)
|
||||
}
|
||||
|
||||
/// Pushes an empty scope layer of local variables.
|
||||
pub(crate) fn push_scope_layer(&mut self) {
|
||||
self.scope.push(Default::default());
|
||||
pub fn push_scope_layer(&mut self) {
|
||||
self.scope.push_layer()
|
||||
}
|
||||
|
||||
/// Pops a scope layer of local variables, if any are available.
|
||||
pub(crate) fn pop_scope_layer(&mut self) -> Option<Locals> {
|
||||
self.scope.pop()
|
||||
pub fn pop_scope_layer(&mut self) -> Option<ScopeLocalSyms> {
|
||||
self.scope.pop_layer()
|
||||
}
|
||||
|
||||
/// Collects local variables for the given AST body non-recursively.
|
||||
|
||||
86
src/compile/scope.rs
Normal file
86
src/compile/scope.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use crate::obj::prelude::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// current TODO/problem: How should locals work?
|
||||
//
|
||||
// * I think we should have a global/local dichotomy in the VM. You can only look up the local
|
||||
// scope in the VM; inner functions that have captures should be created on the fly and detected
|
||||
// by the compiler. I.E. any lookups that are not local or global should be captured dynamically.
|
||||
// * Need to differentiate between locals collected on the compilation side vs. locals on the
|
||||
// object/vm side
|
||||
|
||||
pub type ScopeLocalSyms = BTreeMap<Sym, Name>;
|
||||
pub type ScopeLocals = Vec<Sym>;
|
||||
|
||||
pub struct Scope {
|
||||
scope: Vec<ScopeLocalSyms>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub fn lookup_local(&self, sym: Sym) -> Option<Name> {
|
||||
self.locals()
|
||||
.and_then(|locals| locals.get(&sym))
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn lookup_global(&self, sym: Sym) -> Option<Name> {
|
||||
self.globals()
|
||||
.and_then(|globals| globals.get(&sym))
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn create_local(&mut self, sym: Sym) -> Name {
|
||||
let locals = self.locals_mut()
|
||||
.expect("locals");
|
||||
if let Some(local) = locals.get(&sym).copied() {
|
||||
local
|
||||
} else {
|
||||
let name = Name::new(locals.len());
|
||||
locals.insert(sym, name);
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_global(&mut self, sym: Sym) -> Name {
|
||||
let globals = self.globals_mut()
|
||||
.expect("globals");
|
||||
if let Some(global) = globals.get(&sym).copied() {
|
||||
global
|
||||
} else {
|
||||
let name = Name::new(globals.len());
|
||||
globals.insert(sym, name);
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_layer(&mut self) {
|
||||
self.scope.push(Default::default());
|
||||
}
|
||||
|
||||
pub fn pop_layer(&mut self) -> Option<ScopeLocalSyms> {
|
||||
self.scope.pop()
|
||||
}
|
||||
|
||||
pub fn locals(&self) -> Option<&ScopeLocalSyms> {
|
||||
self.scope.last()
|
||||
}
|
||||
|
||||
pub fn locals_mut(&mut self) -> Option<&mut ScopeLocalSyms> {
|
||||
self.scope.last_mut()
|
||||
}
|
||||
|
||||
pub fn globals(&self) -> Option<&ScopeLocalSyms> {
|
||||
self.scope.first()
|
||||
}
|
||||
|
||||
pub fn globals_mut(&mut self) -> Option<&mut ScopeLocalSyms> {
|
||||
self.scope.first_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Scope {
|
||||
fn default() -> Self {
|
||||
// empty global scope
|
||||
Self { scope: vec![Default::default()] }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
syn::{ast::*, visit::*},
|
||||
vm::inst::*,
|
||||
};
|
||||
use std::mem;
|
||||
use std::{collections::BTreeMap, mem};
|
||||
|
||||
/// A basic block of VM code.
|
||||
///
|
||||
@@ -213,9 +213,7 @@ impl<'c> CompileBody<'c> {
|
||||
}
|
||||
|
||||
pub fn compile(&mut self, body: &'c Body) -> Result<Thunk> {
|
||||
self.compile.push_scope_layer();
|
||||
let thunk = self.visit_body(body)?;
|
||||
self.compile.pop_scope_layer();
|
||||
Ok(thunk)
|
||||
}
|
||||
}
|
||||
@@ -266,8 +264,12 @@ impl Visit for CompileBody<'_> {
|
||||
}
|
||||
LhsExpr::Name(local_name) => {
|
||||
let sym = global_sym(local_name.to_string());
|
||||
let local = self.compile.lookup_scope_or_create_global(sym);
|
||||
thunk = Inst::Pop(Some(local)).into();
|
||||
if let Some(local) = self.compile.lookup_local(sym) {
|
||||
thunk = Inst::PopLocal(Some(local)).into();
|
||||
} else {
|
||||
let global = self.compile.lookup_global(sym).expect("name expected to exist someplace(?)");
|
||||
thunk = Inst::PopGlobal(Some(global)).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(thunk)
|
||||
@@ -346,6 +348,12 @@ impl Visit for CompileBody<'_> {
|
||||
}
|
||||
|
||||
fn visit_fun_expr(&mut self, expr: &FunExpr) -> Self::Out {
|
||||
// TODO(fun) : need captures for functions, built dynamically (or statically?)
|
||||
// - static is not possible, since captures are *created* at runtime, and there's no
|
||||
// instruction that will look up just one scope level - it's either locals or globals.
|
||||
// - an entire "create function" instruction is probably the best way to solve it, don't
|
||||
// try to be clever, just implement it like that (since I mean, python does too...)
|
||||
|
||||
// - push const
|
||||
// (functions are unique const values so a new function will be created for every literal
|
||||
// function defined in code)
|
||||
@@ -357,7 +365,23 @@ impl Visit for CompileBody<'_> {
|
||||
let sym = global_sym(param.to_string());
|
||||
self.compile.create_local(sym);
|
||||
}
|
||||
let locals = self.compile.pop_scope_layer().unwrap();
|
||||
// remap (Sym -> Name) to be (Name -> Sym) and make sure it's all in order.
|
||||
let scope_locals: BTreeMap<_, _> = self.compile.pop_scope_layer()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(sym, name)| (name, sym))
|
||||
.collect();
|
||||
// this should be in numeric order since:
|
||||
// 1. locals are created exactly once or looked up
|
||||
// 2. scope_locals is a btreemap, keyed by names, which are in order from 0..N
|
||||
let locals: FunLocals = scope_locals.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (name, sym))| {
|
||||
assert_eq!(index, name.index());
|
||||
sym
|
||||
})
|
||||
.collect();
|
||||
|
||||
let code = self.visit_body(&expr.body)?
|
||||
.flatten()
|
||||
.to_vec();
|
||||
@@ -373,9 +397,14 @@ impl Visit for CompileBody<'_> {
|
||||
let thunk = match atom {
|
||||
Atom::Ident(ident) => {
|
||||
let sym = global_sym(ident.to_string());
|
||||
let name = self.compile.lookup_scope_or_create_global(sym);
|
||||
// get local
|
||||
Inst::LoadName(name).into()
|
||||
if let Some(local) = self.compile.lookup_local(sym) {
|
||||
// get local
|
||||
Inst::LoadLocal(local).into()
|
||||
} else {
|
||||
// get or create global
|
||||
let global = self.compile.create_global(sym);
|
||||
Inst::LoadGlobal(global).into()
|
||||
}
|
||||
}
|
||||
Atom::Sym(sym) => {
|
||||
// push symbol
|
||||
|
||||
Reference in New Issue
Block a user