Skip to content

Commit

Permalink
Merge pull request #377 from koto-lang/add-exported-map-keys-to-local…
Browse files Browse the repository at this point in the history
…-scope

Add exported map keys to local scope
  • Loading branch information
irh authored Dec 11, 2024
2 parents 5f20e2e + 42b9d8f commit 338c1f6
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 100 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ The Koto project adheres to
- `@||` has been renamed to `@call`, and `@[]` has been renamed to `@index`.
- `:` placement following keys in maps is now more flexible.
([#368](https://github.com/koto-lang/koto/issues/368))
- When a map is used with `export`, the map entries now get added to the
local scope. Local variables with names that match an exported map key
will be updated.

#### Core Library

Expand Down
28 changes: 23 additions & 5 deletions crates/bytecode/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -965,9 +965,7 @@ impl Compiler {
match &target_node.node {
Node::Id(id_index, type_hint) => {
if !value_result.is_temporary {
// To ensure that exported rhs ids with the same name as a local that's
// currently being assigned can be loaded correctly, only commit the
// reserved local as assigned after the rhs has been compiled.
// The ID being assigned to must be committed now that the RHS has been resolved.
self.commit_local_register(value_register)?;
}

Expand Down Expand Up @@ -2242,12 +2240,23 @@ impl Compiler {
for (key, maybe_value_node) in entries.iter() {
let key_node = ctx.node(*key);
let value = match (key_node, maybe_value_node) {
// A value has been provided for the entry
// An ID key with a value, and we're in an export expression
(Node::Id(id, ..), Some(value_node)) if export_entries => {
// The value is being exported, and should be made available in scope
let value_register = self.reserve_local_register(*id)?;
let value_node = *value_node;
let result =
self.compile_node(value_node, ctx.with_fixed_register(value_register))?;
// Commit the register now that the value has been compiled.
self.commit_local_register(value_register)?;
result
}
// A key with a value
(_, Some(value_node)) => {
let value_node = *value_node;
self.compile_node(value_node, ctx.with_any_register())?
}
// ID-only entry, the value should be locally assigned
// An ID key without a value, a value with matching ID should be available
(Node::Id(id, ..), None) => match self.frame().get_local_assigned_register(*id)
{
Some(register) => CompileNodeOutput::with_assigned(register),
Expand Down Expand Up @@ -3971,18 +3980,27 @@ impl Compiler {
.map_err(|e| self.make_error(e))
}

// Used for values that can be assigned directly to a register
fn assign_local_register(&mut self, local: ConstantIndex) -> Result<u8> {
self.frame_mut()
.assign_local_register(local)
.map_err(|e| self.make_error(e))
}

// Used for expressions that are about to evaluated and assigned to a register
//
// After the RHS has been compiled it needs to be committed to be available in the local scope,
// see commit_local_register.
//
// Reserving is necessary to avoid bringing the local's name into scope during the RHS's
// evaluation before it's been assigned.
fn reserve_local_register(&mut self, local: ConstantIndex) -> Result<u8> {
self.frame_mut()
.reserve_local_register(local)
.map_err(|e| self.make_error(e))
}

// Commit a register now that the RHS expression for an assignment has been computed
fn commit_local_register(&mut self, register: u8) -> Result<u8> {
for deferred_op in self
.frame_mut()
Expand Down
46 changes: 45 additions & 1 deletion crates/cli/docs/language_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2363,7 +2363,7 @@ export let foo: Number = -1

When exporting a lot of values, it can be convenient to use map syntax:

```koto,skip_run
```koto
##################
# my_module.koto #
##################
Expand All @@ -2380,6 +2380,49 @@ export
baz: 'baz'
```

Exported values are available anywhere in the module that exported them.

```koto
get_x = ||
# x hasn't been created yet. When the function is called, the runtime
# will check the exports map for a matching value.
x
export x = 123
print! get_x()
check! 123
```

Exports can be accessed and modified directly via [`koto.exports`][koto-exports].

```koto
export a, b = 1, 2
# koto.exports() returns the current module's exports map
print! exports = koto.exports()
check! {a: 1, b: 2}
# Values can be added directly into the exports map
exports.insert 'c', 3
print! c
check! 3
```

Assigning a new value to a variable that was previously exported won't change
the exported value. If you need to update the exported value, then use `export` (or update the exports map via [`koto.exports`][koto-exports]).

```koto
export x = 99
# Reassigning a new value to x doesn't affect the previously exported value
print! x = 123
check! 123
print! koto.exports().x
check! 99
```

### `@test` functions and `@main`

A module can export `@test` functions, which will be automatically run after
Expand Down Expand Up @@ -2437,6 +2480,7 @@ and if `foo.koto` isn't found then the runtime will look for `foo/main.koto`.
[core]: ./core_lib
[immutable]: https://en.wikipedia.org/wiki/Immutable_object
[iterator]: ./core_lib/iterator.md
[koto-exports]: ./core_lib/koto.md#exports
[koto-type]: ./core_lib/koto.md#type
[map-get]: ./core_lib/map.md#get
[map-insert]: ./core_lib/map.md#insert
Expand Down
1 change: 0 additions & 1 deletion crates/koto/tests/koto_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ macro_rules! koto_test {
mod koto_tests {
use super::*;

koto_test!(assignment);
koto_test!(comments);
koto_test!(enums);
koto_test!(io);
Expand Down
68 changes: 39 additions & 29 deletions crates/parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ struct ExpressionContext {
allow_map_block: bool,
// The indentation rules for the current context
expected_indentation: Indentation,
// When true, map entries should be exported, with the keys assigned to local variables.
// This is used in export expressions where a map is used to define the exported items.
export_map_entries: bool,
}

// The indentation that should be expected on following lines for an expression to continue
Expand All @@ -136,41 +139,37 @@ enum Indentation {
}

impl ExpressionContext {
fn permissive() -> Self {
fn restricted() -> Self {
Self {
allow_space_separated_call: true,
allow_linebreaks: true,
allow_space_separated_call: false,
allow_linebreaks: false,
allow_map_block: false,
expected_indentation: Indentation::Greater,
export_map_entries: false,
}
}

fn restricted() -> Self {
fn permissive() -> Self {
Self {
allow_space_separated_call: false,
allow_linebreaks: false,
allow_map_block: false,
expected_indentation: Indentation::Greater,
allow_space_separated_call: true,
allow_linebreaks: true,
..Self::restricted()
}
}

fn inline() -> Self {
Self {
allow_space_separated_call: true,
allow_linebreaks: false,
allow_map_block: false,
expected_indentation: Indentation::Greater,
..Self::restricted()
}
}

// After a keyword like `yield` or `return`.
// Like inline(), but inherits allow_linebreaks
fn start_new_expression(&self) -> Self {
Self {
allow_space_separated_call: true,
allow_linebreaks: self.allow_linebreaks,
allow_map_block: false,
expected_indentation: Indentation::Greater,
..Self::inline()
}
}

Expand All @@ -179,10 +178,8 @@ impl ExpressionContext {
// x = [f x, y] # A single entry list is created with the result of calling `f(x, y)`
fn braced_items_start() -> Self {
Self {
allow_space_separated_call: true,
allow_linebreaks: true,
allow_map_block: false,
expected_indentation: Indentation::Flexible,
..Self::permissive()
}
}

Expand All @@ -194,9 +191,7 @@ impl ExpressionContext {
fn braced_items_continued() -> Self {
Self {
allow_space_separated_call: false,
allow_linebreaks: true,
allow_map_block: false,
expected_indentation: Indentation::Flexible,
..Self::braced_items_start()
}
}

Expand All @@ -215,10 +210,10 @@ impl ExpressionContext {
};

Self {
allow_space_separated_call: self.allow_space_separated_call,
allow_linebreaks: self.allow_linebreaks,
allow_map_block: false,
expected_indentation,
export_map_entries: false,
..*self
}
}

Expand All @@ -228,6 +223,13 @@ impl ExpressionContext {
..*self
}
}

fn with_exported_map_entries(&self) -> Self {
Self {
export_map_entries: true,
..*self
}
}
}

/// Koto's parser
Expand Down Expand Up @@ -1244,6 +1246,9 @@ impl<'source> Parser<'source> {

if id_context.allow_map_block && self.peek_next_token_on_same_line() == Some(Token::Colon) {
// The ID is the start of a map block
if context.export_map_entries {
self.frame_mut()?.add_local_id_assignment(constant_index);
}
self.consume_map_block(id_node, id_span, &id_context)
} else {
self.frame_mut()?.add_id_access(constant_index);
Expand Down Expand Up @@ -1649,9 +1654,10 @@ impl<'source> Parser<'source> {
self.consume_token_with_context(context); // Token::Export
let start_span = self.current_span();

if let Some(expression) =
self.parse_expressions(&ExpressionContext::permissive(), TempResult::No)?
{
if let Some(expression) = self.parse_expressions(
&ExpressionContext::permissive().with_exported_map_entries(),
TempResult::No,
)? {
self.push_node_with_start_span(Node::Export(expression), start_span)
} else {
self.consume_token_and_error(SyntaxError::ExpectedExpression)
Expand Down Expand Up @@ -1877,7 +1883,7 @@ impl<'source> Parser<'source> {
while self.peek_token_with_context(&block_context).is_some() {
self.consume_until_token_with_context(&block_context);

let Some(key) = self.parse_map_key()? else {
let Some(key) = self.parse_map_key(context.export_map_entries)? else {
return self.consume_token_and_error(SyntaxError::ExpectedMapEntry);
};

Expand Down Expand Up @@ -1909,7 +1915,7 @@ impl<'source> Parser<'source> {
let start_indent = self.current_indent();
let start_span = self.current_span();

let entries = self.parse_comma_separated_map_entries()?;
let entries = self.parse_comma_separated_map_entries(context)?;
self.expect_and_consume_token(
Token::CurlyClose,
SyntaxError::ExpectedMapEnd.into(),
Expand All @@ -1925,14 +1931,15 @@ impl<'source> Parser<'source> {

fn parse_comma_separated_map_entries(
&mut self,
context: &ExpressionContext,
) -> Result<AstVec<(AstIndex, Option<AstIndex>)>> {
let mut entries = AstVec::new();
let mut entry_context = ExpressionContext::braced_items_start();

while self.peek_token_with_context(&entry_context).is_some() {
self.consume_until_token_with_context(&entry_context);

let Some(key) = self.parse_map_key()? else {
let Some(key) = self.parse_map_key(context.export_map_entries)? else {
break;
};

Expand Down Expand Up @@ -1986,8 +1993,11 @@ impl<'source> Parser<'source> {
// regular_id: 1
// 'string_id': 2
// @meta meta_key: 3
fn parse_map_key(&mut self) -> Result<Option<AstIndex>> {
fn parse_map_key(&mut self, export_map_entry: bool) -> Result<Option<AstIndex>> {
let result = if let Some((id, _)) = self.parse_id(&ExpressionContext::restricted())? {
if export_map_entry {
self.frame_mut()?.add_local_id_assignment(id);
}
Some(self.push_node(Node::Id(id, None))?)
} else if let Some(s) = self.parse_string(&ExpressionContext::restricted())? {
Some(self.push_node_with_span(Node::Str(s.string), s.span)?)
Expand Down
2 changes: 1 addition & 1 deletion crates/parser/tests/parser_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1625,7 +1625,7 @@ export
Export(4.into()), // 5
MainBlock {
body: nodes(&[5]),
local_count: 0,
local_count: 2,
},
],
Some(&[Constant::Str("a"), Constant::Str("b")]),
Expand Down
12 changes: 11 additions & 1 deletion crates/runtime/tests/vm_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3801,11 +3801,21 @@ x + y";
let script = "
export
x: 1
y: 2
y: x + 1
x + y
";
check_script_output(script, 3);
}

#[test]
fn map_export_of_previously_assigned_variable() {
let script = "
x = 99
export {x: 1} # The local variable should be updated
x
";
check_script_output(script, 1);
}
}

mod meta_export {
Expand Down
1 change: 1 addition & 0 deletions crates/test_utils/src/doc_examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ An example in '{}' failed to compile: {error}",
skip_check: bool,
) -> Result<()> {
self.output.clear();
self.vm.exports_mut().clear();

let chunk = self.compile_example(script, sections)?;

Expand Down
Loading

0 comments on commit 338c1f6

Please sign in to comment.