Skip to content

Commit

Permalink
Add support for const decl variables
Browse files Browse the repository at this point in the history
* Adds new keyword 'const' which should in theory allow a variable to be
assigned to only once in the given lexical scope.
* Add a test for disabled lexical scoping
* Also clarifies the CompileError more specifically
  • Loading branch information
rdaum committed Aug 12, 2024
1 parent 915f52a commit 628c978
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 48 deletions.
4 changes: 3 additions & 1 deletion crates/compiler/src/decompile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,9 @@ pub fn program_to_tree(program: &Program) -> Result<Parse, DecompileError> {
let mut unbound_to_bound = HashMap::new();
for bound_name in program.var_names.names() {
let n = program.var_names.name_of(&bound_name).unwrap();
let ub = unbound_names.find_or_add_name_global(n.as_str());
let ub = unbound_names
.find_or_add_name_global(n.as_str())
.map_err(|_| DecompileError::NameNotFound(bound_name))?;
bound_to_unbound.insert(bound_name, ub);
unbound_to_bound.insert(ub, bound_name);
}
Expand Down
2 changes: 2 additions & 0 deletions crates/compiler/src/moo.pest
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ statement = {
| begin_statement
| expr_statement
| local_assignment
| const_assignment
| global_assignment
}

Expand Down Expand Up @@ -59,6 +60,7 @@ unlabelled_except = { "(" ~ codes ~ ")" }
begin_statement = { ^"begin" ~ statements ~ ^"end" }

local_assignment = { ^"let" ~ ident ~ (ASSIGN ~ expr)? ~ ";" }
const_assignment = { ^"const" ~ ident ~ (ASSIGN ~ expr)? ~ ";" }

// globally scoped (same as default in MOO) adds explicitly to global scope.
global_assignment = { ^"global" ~ ident ~ (ASSIGN ~ expr)? ~ ";" }
Expand Down
104 changes: 83 additions & 21 deletions crates/compiler/src/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,46 @@

use crate::GlobalName;
use bincode::{Decode, Encode};
use moor_values::model::CompileError;
use moor_values::var::Symbol;
use std::collections::HashMap;
use strum::IntoEnumIterator;

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct UnboundNames {
unbound_names: Vec<(Symbol, usize)>,
unbound_names: Vec<Decl>,
scope: Vec<HashMap<Symbol, UnboundName>>,
}

#[derive(Debug, Clone, Eq, PartialEq)]
struct Decl {
sym: Symbol,
depth: usize,
constant: bool,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct UnboundName(usize);
pub struct UnboundName {
offset: usize,
pub sym: Symbol,
pub is_constant: bool,
}

impl Default for UnboundNames {
fn default() -> Self {
Self::new()
}
}

/// Policy for binding a variable when new_bound is called.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum BindMode {
/// If the variable is already bound, use it.
Reuse,
/// If the variable exists, return an error.
New,
}

impl UnboundNames {
pub fn new() -> Self {
let mut names = Self {
Expand All @@ -41,27 +62,31 @@ impl UnboundNames {
scope: vec![HashMap::new()],
};
for global in GlobalName::iter() {
names.find_or_add_name_global(global.to_string().as_str());
names
.find_or_add_name_global(global.to_string().as_str())
.unwrap();
}
names
}

/// Find a variable name, or declare in global scope.
pub fn find_or_add_name_global(&mut self, name: &str) -> UnboundName {
pub fn find_or_add_name_global(&mut self, name: &str) -> Result<UnboundName, CompileError> {
let name = Symbol::mk_case_insensitive(name);

// Check the scopes, starting at the back (innermost scope)
for scope in self.scope.iter().rev() {
if let Some(n) = scope.get(&name) {
return *n;
return Ok(*n);
}
}

// If the name doesn't exist, add it to the global scope, since that's how
// MOO currently works.
let unbound_name = self.new_unbound(name, 0);
// These types of variables are always mutable, and always re-use a variable name, to
// maintain existing MOO language semantics.
let unbound_name = self.new_unbound(name, 0, false, BindMode::Reuse)?;
self.scope[0].insert(name, unbound_name);
unbound_name
Ok(unbound_name)
}

/// Start a new lexical scope.
Expand All @@ -75,21 +100,33 @@ impl UnboundNames {
scope.len()
}

/// Declare a name in the current lexical scope.
pub fn declare_name(&mut self, name: &str) -> UnboundName {
/// Declare a (mutable) name in the current lexical scope.
pub fn declare_name(&mut self, name: &str) -> Result<UnboundName, CompileError> {
let name = Symbol::mk_case_insensitive(name);
let unbound_name = self.new_unbound(name, self.scope.len() - 1);
let unbound_name = self.new_unbound(name, self.scope.len() - 1, false, BindMode::New)?;
self.scope.last_mut().unwrap().insert(name, unbound_name);
unbound_name
Ok(unbound_name)
}

/// Declare a (mutable) name in the current lexical scope.
pub fn declare_const(&mut self, name: &str) -> Result<UnboundName, CompileError> {
let name = Symbol::mk_case_insensitive(name);
let unbound_name = self.new_unbound(name, self.scope.len() - 1, true, BindMode::New)?;
self.scope.last_mut().unwrap().insert(name, unbound_name);
Ok(unbound_name)
}

/// If the same named variable exists in multiple scopes, return them all as a vector.
pub fn find_named(&self, name: &str) -> Vec<UnboundName> {
let name = Symbol::mk_case_insensitive(name);
let mut names = vec![];
for (i, n) in self.unbound_names.iter().enumerate() {
if n.0 == name {
names.push(UnboundName(i));
for (i, Decl { sym, constant, .. }) in self.unbound_names.iter().enumerate() {
if *sym == name {
names.push(UnboundName {
offset: i,
sym: *sym,
is_constant: *constant,
});
}
}
names
Expand All @@ -107,10 +144,28 @@ impl UnboundNames {
}

/// Create a new unbound variable.
fn new_unbound(&mut self, name: Symbol, scope: usize) -> UnboundName {
fn new_unbound(
&mut self,
name: Symbol,
scope: usize,
constant: bool,
bind_mode: BindMode,
) -> Result<UnboundName, CompileError> {
// If the variable already exists in this scope, return an error.
if bind_mode == BindMode::New && self.scope[scope].contains_key(&name) {
return Err(CompileError::DuplicateVariable(name));
}
let idx = self.unbound_names.len();
self.unbound_names.push((name, scope));
UnboundName(idx)
self.unbound_names.push(Decl {
sym: name,
depth: scope,
constant,
});
Ok(UnboundName {
offset: idx,
sym: name,
is_constant: constant,
})
}

/// Turn all unbound variables into bound variables.
Expand All @@ -122,11 +177,18 @@ impl UnboundNames {
// This will produce offsets for all variables in the order they should appear in the
// environment.
let mut scope_depth = Vec::with_capacity(self.unbound_names.len());
for idx in 0..self.unbound_names.len() {
for (idx, decl) in self.unbound_names.iter().enumerate() {
let offset = bound.len();
bound.push(self.unbound_names[idx].0);
scope_depth.push(self.unbound_names[idx].1 as u16);
mapping.insert(UnboundName(idx), Name(offset as u16));
bound.push(self.unbound_names[idx].sym);
scope_depth.push(self.unbound_names[idx].depth as u16);
mapping.insert(
UnboundName {
offset: idx,
sym: decl.sym,
is_constant: decl.constant,
},
Name(offset as u16),
);
}

let global_width = self.scope[0].len();
Expand Down
Loading

0 comments on commit 628c978

Please sign in to comment.