From c5d413c8e7cbcf6f27ef509bff2cc0c555a3fdca Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Wed, 11 Dec 2024 14:52:56 +0100 Subject: [PATCH 1/4] Add some comments to the compiler --- crates/bytecode/src/compiler.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/bytecode/src/compiler.rs b/crates/bytecode/src/compiler.rs index 2f2f3d5b..2ea136e9 100644 --- a/crates/bytecode/src/compiler.rs +++ b/crates/bytecode/src/compiler.rs @@ -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)?; } @@ -3971,18 +3969,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 { 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 { 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 { for deferred_op in self .frame_mut() From 1825e4a298643a0576619934427c032152e4b0ec Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Wed, 11 Dec 2024 14:53:32 +0100 Subject: [PATCH 2/4] Remove an unnecessary test file --- crates/koto/tests/koto_tests.rs | 1 - koto/tests/assignment.koto | 61 --------------------------------- 2 files changed, 62 deletions(-) delete mode 100644 koto/tests/assignment.koto diff --git a/crates/koto/tests/koto_tests.rs b/crates/koto/tests/koto_tests.rs index b717e407..61adf933 100644 --- a/crates/koto/tests/koto_tests.rs +++ b/crates/koto/tests/koto_tests.rs @@ -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); diff --git a/koto/tests/assignment.koto b/koto/tests/assignment.koto deleted file mode 100644 index bc971bbf..00000000 --- a/koto/tests/assignment.koto +++ /dev/null @@ -1,61 +0,0 @@ -export - @test basic_assignment: || - a = 1 - b = -a - assert_eq a, -b - - @test multi_assignment: || - a, b, c, d, e = 1, 2, 3, 4, 5, 6, 7, 8, - assert_eq c, 3 - assert_eq e, 5 - - @test chained_assignment: || - a = b = "foo" - assert_eq a, "foo" - assert_eq b, "foo" - - @test unicode_identifiers: || - やあ = héllø = 99 - assert_eq héllø, 99 - assert_eq やあ, 99 - - @test assignment_returns_value: || - assert_eq (a = 42), 42 - assert_eq (x = 99), 99 - assert_eq a, 42 - assert_eq x, 99 - - @test export_assignment: || - f = || - export x = 42 - f() - assert_eq x, 42 - - f2 = || - export x = x * 2 - f2() - assert_eq x, 84 - - f3 = || - x = x + 15 # assigning x in local scope - assert_eq x, 99 - f3() - assert_eq x, 84 # exported x remains the same - - @test multiline_assignment: || - f = |n| n - a, b, c = - 1, - (f 2), - (f 3), - assert_eq a, 1 - assert_eq b, 2 - assert_eq c, 3 - - @test assign_null: || - a = null - assert_eq a, null - assert_ne 1, null - - b = () # Empty parentheses resolve to null - assert_eq a, b From 63ed603b1eba36a80a18afe4f33ed8a98b499195 Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Wed, 11 Dec 2024 15:29:46 +0100 Subject: [PATCH 3/4] Add some extra export examples to the guide --- crates/cli/docs/language_guide.md | 46 ++++++++++++++++++++++++++- crates/test_utils/src/doc_examples.rs | 1 + 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/cli/docs/language_guide.md b/crates/cli/docs/language_guide.md index 5cd638ca..7b277e81 100644 --- a/crates/cli/docs/language_guide.md +++ b/crates/cli/docs/language_guide.md @@ -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 # ################## @@ -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 @@ -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 diff --git a/crates/test_utils/src/doc_examples.rs b/crates/test_utils/src/doc_examples.rs index 015b5d57..6a787a70 100644 --- a/crates/test_utils/src/doc_examples.rs +++ b/crates/test_utils/src/doc_examples.rs @@ -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)?; From 42b9d8fbd0df7203f89d08953812745afe3fc019 Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Wed, 11 Dec 2024 15:32:57 +0100 Subject: [PATCH 4/4] Ensure that values exported via map are assigned in the local scope This makes maps consistent with export assignment: exported values should be available locally, and should update matching local variables. --- CHANGELOG.md | 3 ++ crates/bytecode/src/compiler.rs | 15 ++++++- crates/parser/src/parser.rs | 68 +++++++++++++++++------------ crates/parser/tests/parser_tests.rs | 2 +- crates/runtime/tests/vm_tests.rs | 12 ++++- koto/tests/import.koto | 6 +++ koto/tests/test_module/main.koto | 6 ++- 7 files changed, 78 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e9f553..fe65e0b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/bytecode/src/compiler.rs b/crates/bytecode/src/compiler.rs index 2ea136e9..e88adc9c 100644 --- a/crates/bytecode/src/compiler.rs +++ b/crates/bytecode/src/compiler.rs @@ -2240,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), diff --git a/crates/parser/src/parser.rs b/crates/parser/src/parser.rs index c8b9009d..cdba7e96 100644 --- a/crates/parser/src/parser.rs +++ b/crates/parser/src/parser.rs @@ -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 @@ -136,30 +139,28 @@ 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() } } @@ -167,10 +168,8 @@ impl ExpressionContext { // 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() } } @@ -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() } } @@ -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() } } @@ -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 } } @@ -228,6 +223,13 @@ impl ExpressionContext { ..*self } } + + fn with_exported_map_entries(&self) -> Self { + Self { + export_map_entries: true, + ..*self + } + } } /// Koto's parser @@ -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); @@ -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) @@ -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); }; @@ -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(), @@ -1925,6 +1931,7 @@ impl<'source> Parser<'source> { fn parse_comma_separated_map_entries( &mut self, + context: &ExpressionContext, ) -> Result)>> { let mut entries = AstVec::new(); let mut entry_context = ExpressionContext::braced_items_start(); @@ -1932,7 +1939,7 @@ impl<'source> Parser<'source> { 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; }; @@ -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> { + fn parse_map_key(&mut self, export_map_entry: bool) -> Result> { 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)?) diff --git a/crates/parser/tests/parser_tests.rs b/crates/parser/tests/parser_tests.rs index fad15e18..d03e7929 100644 --- a/crates/parser/tests/parser_tests.rs +++ b/crates/parser/tests/parser_tests.rs @@ -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")]), diff --git a/crates/runtime/tests/vm_tests.rs b/crates/runtime/tests/vm_tests.rs index 97b3a610..d9be391f 100644 --- a/crates/runtime/tests/vm_tests.rs +++ b/crates/runtime/tests/vm_tests.rs @@ -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 { diff --git a/koto/tests/import.koto b/koto/tests/import.koto index 5261f94b..a1daba66 100644 --- a/koto/tests/import.koto +++ b/koto/tests/import.koto @@ -60,6 +60,12 @@ export import "test_module/baz" as baz assert_eq baz.qux, "O_o" + @test import_item_exported_with_string_id: || + assert_eq test_module.exported_with_string_id, 99 + + @test import_item_exported_via_inserting_into_exports_map: || + assert_eq test_module.exported_via_insert, -123 + @test tests_should_be_run_when_importing_a_module: || # Tests will be run when importing a module when the 'run import tests' setting is set # in the runtime. diff --git a/koto/tests/test_module/main.koto b/koto/tests/test_module/main.koto index db7f410d..f08d732c 100644 --- a/koto/tests/test_module/main.koto +++ b/koto/tests/test_module/main.koto @@ -10,8 +10,12 @@ export let square: Function = |x| x * x # Export with a map block export - baz: import baz # Re-export the neighbouring baz module tests_were_run: false + baz: import baz # Re-export the neighbouring baz module + 'exported_with_string_id': 99 + +# Export by inserting an entry to koto.exports() +koto.exports().insert 'exported_via_insert', -123 # Metakeys can be assigned to directly @type = 'test_module'