Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement static class fields #170

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ With the syntax and implementation changes so far the Locks language has divered
- Add [`instanceof`](https://kyleect.github.io/locks/#/docs#instanceof) native function to return `boolean` if the value is an instance of the class or super class.
- Add the base class [`Object`](https://kyleect.github.io/locks/#/docs#classes-object) class that all classes extend from.
- The file `res/lib/locks.locks` is loaded by the VM before running user code. This is where the base class `Object` is defined.
- Implement [`static class fields`](https://kyleect.github.io/locks/#/docs#classes-static-fields)
- Bug Fixes
- Add `#[repr(C)]` to `ObjectNative`. This fixes a segfault that occurred when there were multiple entries in the `Native` enum.
- [Remove an OP transformation the compiler](https://github.com/kyleect/locks/pull/135/files#diff-23c5734d7de815d5e64ad2291873d96e9f686a8b11d76481f3d02c905c53341dL403) was doing that would cause a segfault when bound methods were passed to functions e.g. `function(instance.method)`
Expand Down
22 changes: 22 additions & 0 deletions playground/src/pages/docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ const Docs: React.FC = () => (
Class Field/Method Index Access
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="#classes-static-fields">
Static Class Fields
</Link>
</li>
<li className="nav-item">
<Link
className="nav-link"
Expand Down Expand Up @@ -533,6 +538,23 @@ const Docs: React.FC = () => (
height="300px"
/>

<DocCard
title="Static Class Fields"
anchor="classes-static-fields"
code={[
'class Test {',
' static let value = 123;',
'}',
'',
'println(Test.value); // out: 123',
'',
'Test.value = 1000;',
'',
'println(Test.value);',
]}
height="200px"
/>

<DocCard
title="Single Expression Method Bodies"
anchor="classes-single-expression-method-bodies"
Expand Down
2 changes: 1 addition & 1 deletion res/examples/field/get_on_class.locks
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
class Foo {}
Foo.bar; // out: AttributeError: "class" object has no attribute "bar"
Foo.bar; // out: AttributeError: "Foo" object has no attribute "bar"
2 changes: 1 addition & 1 deletion res/examples/field/set_on_class.locks
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
class Foo {}
Foo.bar = "value"; // out: AttributeError: "class" object has no attribute "bar"
Foo.bar = "value"; // out: AttributeError: "Foo" object has no attribute "bar"
5 changes: 5 additions & 0 deletions res/examples/static_field/get_static_field.locks
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Tests {
static let value = 123;
}

println(Tests.value); // out: 123
7 changes: 7 additions & 0 deletions res/examples/static_field/set_static_field.locks
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Tests {
static let value = 123;
}

Tests.value = 1000;

println(Tests.value); // out: 1000
2 changes: 2 additions & 0 deletions res/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ DeclClass: ast::Stmt = <class:StmtClass> => ast::Stmt::Class(<>);

StmtClass: ast::StmtClass =
"class" <name:identifier> <super_:("extends" <Spanned<ExprIdentifier>>)?> "{"
<static_fields:("static" <Spanned<StmtAssign>>)*>
<fields:(<Spanned<StmtAssign>>)*>
<methods:(<Spanned<StmtFn>>)*>
"}" =>
Expand Down Expand Up @@ -395,5 +396,6 @@ extern {
"while" => lexer::Token::While,
"extends" => lexer::Token::Extends,
"package" => lexer::Token::Package,
"static" => lexer::Token::Static
}
}
1 change: 1 addition & 0 deletions src/syntax/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub struct StmtClass {
pub super_: Option<ExprS>,
pub methods: Vec<Spanned<StmtFn>>,
pub fields: Vec<Spanned<StmtAssign>>,
pub static_fields: Vec<Spanned<StmtAssign>>,
}

/// An expression statement evaluates an expression and discards the result.
Expand Down
2 changes: 2 additions & 0 deletions src/syntax/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ pub enum Token {
Extends,
#[token("package")]
Package,
#[token("static")]
Static,

#[regex(r"//.*", logos::skip)]
#[regex(r"[ \r\n\t\f]+", logos::skip)]
Expand Down
30 changes: 30 additions & 0 deletions src/vm/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,36 @@ impl Compiler {
self.emit_u8(op::POP, span);
}

// Initialize class fields, if they exist
if !class.static_fields.is_empty() {
// Get class `Value` by name and push it on to the VM's stack
self.get_variable(
&Identifier { name: class.name.clone(), package: None, depth: None },
span,
gc,
)?;

for (field_assign, span) in &class.static_fields {
let name = &field_assign.identifier.name;

// Compile value expression if it exists and push it on the VM's stack.
//
// This value is the field's default value.
// Otherwise its initalized to `nil`.
match &field_assign.value {
Some(value) => self.compile_expr(value, gc)?,
None => self.emit_u8(op::NIL, span),
}

self.emit_u8(op::STATIC_FIELD, span);
// Emit field name's constant index
let name = gc.alloc(name).into();
self.emit_constant(name, span)?;
}

self.emit_u8(op::POP, span);
}

// Initialize class methods, if they exist
if !class.methods.is_empty() {
self.get_variable(
Expand Down
5 changes: 5 additions & 0 deletions src/vm/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ impl Gc {
self.mark(name);
self.mark(method);
}

for (&name, &static_field) in unsafe { &(*class).static_fields } {
self.mark(name);
self.mark(static_field);
}
}
ObjectType::Closure => {
let closure = unsafe { object.closure };
Expand Down
178 changes: 125 additions & 53 deletions src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ impl VM {
op::GET_INDEX => self.op_get_index(),
op::SET_INDEX => self.op_set_index(),
op::PACKAGE => self.op_package(),
op::STATIC_FIELD => self.op_static_field(),
_ => util::unreachable(),
}?;

Expand Down Expand Up @@ -447,79 +448,137 @@ impl VM {

fn op_get_property(&mut self) -> Result<()> {
let name = unsafe { self.read_value().as_object().string };
let instance = {
let value = unsafe { *self.peek(0) };
let object = value.as_object();
let value = unsafe { *self.peek(0) };

if value.is_object() && object.type_() == ObjectType::Instance {
unsafe { object.instance }
} else {
if !value.is_object() {
return self.err(AttributeError::NoSuchAttribute {
type_: value.type_().to_string(),
name: unsafe { (*name).value.to_string() },
});
}

let object = value.as_object();

match object.type_() {
ObjectType::Class => {
let class = unsafe { object.class };

match unsafe { (*class).static_fields.get(&name) } {
Some(&field) => {
self.pop();
self.push(field);
}
None => {
return self.err(AttributeError::NoSuchAttribute {
type_: unsafe { (*(*class).name).value.to_string() },
name: unsafe { (*name).value.to_string() },
});
}
}
}
ObjectType::Instance => {
let instance = unsafe { object.instance };

match unsafe { (*instance).fields.get(&name) } {
Some(&field) => {
self.pop();
self.push(field);
}
None => match unsafe { (*(*instance).class).get_method(name) } {
Some(&method) => {
let bound_method = self.alloc(ObjectBoundMethod::new(instance, method));
self.pop();
self.push(bound_method.into());
}
None => {
return self.err(AttributeError::NoSuchAttribute {
type_: unsafe { (*(*(*instance).class).name).value.to_string() },
name: unsafe { (*name).value.to_string() },
});
}
},
}
}
_ => {
return self.err(AttributeError::NoSuchAttribute {
type_: value.type_().to_string(),
name: unsafe { (*name).value.to_string() },
});
}
};

match unsafe { (*instance).fields.get(&name) } {
Some(&field) => {
self.pop();
self.push(field);
}
None => match unsafe { (*(*instance).class).get_method(name) } {
Some(&method) => {
let bound_method = self.alloc(ObjectBoundMethod::new(instance, method));
self.pop();
self.push(bound_method.into());
}
None => {
return self.err(AttributeError::NoSuchAttribute {
type_: unsafe { (*(*(*instance).class).name).value.to_string() },
name: unsafe { (*name).value.to_string() },
});
}
},
}

Ok(())
}

fn op_set_property(&mut self) -> Result<()> {
let name = unsafe { self.read_value().as_object().string };
let instance = {
let value = self.pop();
let object = value.as_object();
let value = self.pop();

if value.is_object() && object.type_() == ObjectType::Instance {
unsafe { object.instance }
} else {
return self.err(AttributeError::NoSuchAttribute {
type_: value.type_().to_string(),
if !value.is_object() {
return self.err(AttributeError::NoSuchAttribute {
type_: value.type_().to_string(),
name: unsafe { (*name).value.to_string() },
});
}

let object = value.as_object();

match object.type_() {
ObjectType::Class => {
let class = unsafe { object.class };
let value = unsafe { *self.peek(0) };
let has_field = unsafe { (*class).static_fields.get(&name) };

if let Some(_) = has_field {
unsafe { (*class).static_fields.insert(name, value) };
return Ok(());
}

self.err(AttributeError::NoSuchAttribute {
type_: unsafe { (*(*class).name).value.to_string() },
name: unsafe { (*name).value.to_string() },
});
})
}
};
let value = unsafe { *self.peek(0) };
let has_field = unsafe { (*instance).fields.get(&name) };
ObjectType::Instance => {
let instance = {
if value.is_object() && object.type_() == ObjectType::Instance {
unsafe { object.instance }
} else {
return self.err(AttributeError::NoSuchAttribute {
type_: value.type_().to_string(),
name: unsafe { (*name).value.to_string() },
});
}
};
let value = unsafe { *self.peek(0) };
let has_field = unsafe { (*instance).fields.get(&name) };

if let Some(_) = has_field {
unsafe { (*instance).fields.insert(name, value) };
return Ok(());
}
if let Some(_) = has_field {
unsafe { (*instance).fields.insert(name, value) };
return Ok(());
}

let class_has_method = unsafe { (*(*instance).class).methods.get(&name) };
let class_has_method = unsafe { (*(*instance).class).methods.get(&name) };

if let Some(_) = class_has_method {
return self.err(TypeError::InvalidMethodAssignment {
name: unsafe { (*name).value.to_owned() },
type_: unsafe { (*(*(*instance).class).name).value.to_owned() },
});
}
if let Some(_) = class_has_method {
return self.err(TypeError::InvalidMethodAssignment {
name: unsafe { (*name).value.to_owned() },
type_: unsafe { (*(*(*instance).class).name).value.to_owned() },
});
}

self.err(AttributeError::NoSuchAttribute {
type_: unsafe { (*(*(*instance).class).name).value.to_string() },
name: unsafe { (*name).value.to_string() },
})
self.err(AttributeError::NoSuchAttribute {
type_: unsafe { (*(*(*instance).class).name).value.to_string() },
name: unsafe { (*name).value.to_string() },
})
}
_ => {
return self.err(AttributeError::NoSuchAttribute {
type_: value.type_().to_string(),
name: unsafe { (*name).value.to_string() },
});
}
}
}

fn op_get_super(&mut self) -> Result<()> {
Expand Down Expand Up @@ -795,6 +854,19 @@ impl VM {
Ok(())
}

/// Define a static field on the [`ObjectClass`] on the top of the VM's stack
///
/// This consumes 2 byte ops for field name, & access modifier
///
/// This pop's the [`Value`] from the VM's stack for the field value.
fn op_static_field(&mut self) -> Result<()> {
let name = unsafe { self.read_value().as_object().string };
let value = self.pop();
let class = unsafe { (*self.peek(0)).as_object().class };
unsafe { (*class).static_fields.insert(name, value) };
Ok(())
}

/// Define a method on the [`ObjectClass`] on the top of the VM's stack
///
/// This consumes 1 byte op for method name.
Expand Down
10 changes: 9 additions & 1 deletion src/vm/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,20 @@ pub struct ObjectClass {
pub super_: Option<*mut ObjectClass>,
pub methods: HashMap<*mut ObjectString, *mut ObjectClosure, BuildHasherDefault<FxHasher>>,
pub fields: HashMap<*mut ObjectString, Value, BuildHasherDefault<FxHasher>>,
pub static_fields: HashMap<*mut ObjectString, Value, BuildHasherDefault<FxHasher>>,
}

impl ObjectClass {
pub fn new(name: *mut ObjectString) -> Self {
let common = ObjectCommon { type_: ObjectType::Class, is_marked: false };
Self { common, name, super_: None, methods: HashMap::default(), fields: HashMap::default() }
Self {
common,
name,
super_: None,
methods: HashMap::default(),
fields: HashMap::default(),
static_fields: HashMap::default(),
}
}

/// Get a list of parent/super classes the class extends
Expand Down
3 changes: 2 additions & 1 deletion src/vm/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ iota! {
GET_INDEX,
SET_INDEX,
// Reads a 1 byte offset for the package name
PACKAGE
PACKAGE,
STATIC_FIELD
}
Loading