diff --git a/src/language/core/Semantics/Binding/InteractiveContext.cs b/src/language/core/Semantics/Binding/InteractiveContext.cs new file mode 100644 index 00000000..417ecbd0 --- /dev/null +++ b/src/language/core/Semantics/Binding/InteractiveContext.cs @@ -0,0 +1,67 @@ +namespace Vezel.Celerity.Language.Semantics.Binding; + +public sealed partial class InteractiveContext +{ + public static InteractiveContext Default { get; } = + new(ImmutableDictionary.Empty, ImmutableDictionary.Empty); + + private readonly ImmutableDictionary _uses; + + private readonly ImmutableDictionary _symbols; + + private InteractiveContext( + ImmutableDictionary uses, ImmutableDictionary symbols) + { + _uses = uses; + _symbols = symbols; + } + + public bool TryGetUse(string name, [MaybeNullWhen(false)] out ModulePath path) + { + Check.Argument(UseRegex().IsMatch(name), name); + + return _uses.TryGetValue(name, out path); + } + + public InteractiveContext AddUse(string name, ModulePath path) + { + Check.Argument(UseRegex().IsMatch(name), name); + Check.Null(path); + + return new(_uses.Add(name, path), _symbols); + } + + public InteractiveContext RemoveUse(string name) + { + Check.Argument(UseRegex().IsMatch(name), name); + + return new(_uses.Remove(name), _symbols); + } + + public bool TryGetSymbol(string name, [MaybeNullWhen(false)] out InteractiveSymbol symbol) + { + Check.Argument(SymbolRegex().IsMatch(name), name); + + return _symbols.TryGetValue(name, out symbol); + } + + public InteractiveContext AddSymbol(string name, bool mutable) + { + Check.Argument(SymbolRegex().IsMatch(name), name); + + return new(_uses, _symbols.Add(name, new(name, mutable))); + } + + public InteractiveContext RemoveSymbol(string name) + { + Check.Argument(SymbolRegex().IsMatch(name), name); + + return new(_uses, _symbols.Remove(name)); + } + + [GeneratedRegex(@"^[A-Z][a-zA-Z0-9]*$", RegexOptions.Singleline | RegexOptions.CultureInvariant)] + private static partial Regex UseRegex(); + + [GeneratedRegex(@"^[a-z][_0-9a-z]*$", RegexOptions.Singleline | RegexOptions.CultureInvariant)] + private static partial Regex SymbolRegex(); +} diff --git a/src/language/core/Semantics/Binding/InteractiveSymbol.cs b/src/language/core/Semantics/Binding/InteractiveSymbol.cs new file mode 100644 index 00000000..e582f922 --- /dev/null +++ b/src/language/core/Semantics/Binding/InteractiveSymbol.cs @@ -0,0 +1,25 @@ +using Vezel.Celerity.Language.Semantics.Tree; +using Vezel.Celerity.Language.Syntax.Tree; + +namespace Vezel.Celerity.Language.Semantics.Binding; + +public sealed class InteractiveSymbol : LocalSymbol +{ + public override bool IsMutable => _mutable; + + public override bool IsDiscard => false; + + private readonly bool _mutable; + + internal InteractiveSymbol(string name, bool mutable) + : base(name) + { + _mutable = mutable; + } + + private protected override SyntaxToken GetToken(SemanticNode node) + { + // We never have bindings, so this should never be called. + throw new UnreachableException(); + } +} diff --git a/src/language/core/Semantics/LanguageAnalyzer.cs b/src/language/core/Semantics/LanguageAnalyzer.cs index 5c68816c..4a653150 100644 --- a/src/language/core/Semantics/LanguageAnalyzer.cs +++ b/src/language/core/Semantics/LanguageAnalyzer.cs @@ -32,6 +32,8 @@ public void Dispose() private readonly SyntaxTree _tree; + private readonly InteractiveContext _context; + private readonly ImmutableArray.Builder _diagnostics; private readonly Dictionary Declarations, ModulePath? Path)> _uses = @@ -43,9 +45,11 @@ public void Dispose() private Scope _scope = new(null); - public AnalysisVisitor(SyntaxTree tree, ImmutableArray.Builder diagnostics) + public AnalysisVisitor( + SyntaxTree tree, InteractiveContext context, ImmutableArray.Builder diagnostics) { _tree = tree; + _context = context; _diagnostics = diagnostics; } @@ -148,6 +152,7 @@ private ScopeContext PushScope() { 0 => (null, null), 1 when _uses.TryGetValue(comps[0], out var tup) => (tup.Declarations[^1], tup.Path), + 1 when _context.TryGetUse(comps[0], out var p) => (null, p), _ => (null, new(comps)), }; } @@ -964,6 +969,9 @@ public override IdentifierExpressionSemantics VisitIdentifierExpression(Identifi { sym = _scope.ResolveSymbol(ident.Text); + if (sym == null && _context.TryGetSymbol(ident.Text, out var sym2)) + sym = sym2; + if (sym == null) Error( ident.Span, @@ -1234,9 +1242,10 @@ public override SetPatternSemantics VisitSetPattern(SetPatternSyntax node) private readonly AnalysisVisitor _visitor; - public LanguageAnalyzer(SyntaxTree tree, ImmutableArray.Builder diagnostics) + public LanguageAnalyzer( + SyntaxTree tree, InteractiveContext context, ImmutableArray.Builder diagnostics) { - _visitor = new(tree, diagnostics); + _visitor = new(tree, context, diagnostics); } public DocumentSemantics Analyze() diff --git a/src/language/core/Semantics/SemanticTree.cs b/src/language/core/Semantics/SemanticTree.cs index eafa70cf..b7bc4c3d 100644 --- a/src/language/core/Semantics/SemanticTree.cs +++ b/src/language/core/Semantics/SemanticTree.cs @@ -1,6 +1,8 @@ using Vezel.Celerity.Language.Diagnostics; +using Vezel.Celerity.Language.Semantics.Binding; using Vezel.Celerity.Language.Semantics.Tree; using Vezel.Celerity.Language.Syntax; +using Vezel.Celerity.Language.Syntax.Tree; namespace Vezel.Celerity.Language.Semantics; @@ -21,12 +23,13 @@ private SemanticTree(SyntaxTree syntax, DocumentSemantics root, ImmutableArray(0); - var root = new LanguageAnalyzer(syntax, diags).Analyze(); + var root = new LanguageAnalyzer(syntax, context ?? InteractiveContext.Default, diags).Analyze(); diags.Sort(static (x, y) => x.Span.CompareTo(y.Span)); diff --git a/src/tests/QualityTests.cs b/src/tests/QualityTests.cs index 1ffe0a85..81301326 100644 --- a/src/tests/QualityTests.cs +++ b/src/tests/QualityTests.cs @@ -6,14 +6,28 @@ public sealed partial class QualityTests : CelerityTests { private static Task TestAsync( - SyntaxMode mode, + string contents, LintPass pass, [CallerFilePath] string file = "", [CallerMemberName] string name = "") + { + return TestAsync(default(InteractiveContext), contents, pass, file, name); + } + + private static Task TestAsync( + Func contextSetup, string contents, LintPass pass, [CallerFilePath] string file = "", [CallerMemberName] string name = "") { - var syntax = SyntaxTree.Parse(new StringSourceText($"{name}.cel", contents), mode, discardText: true); - var semantics = SemanticTree.Analyze(syntax); + return TestAsync(contextSetup(InteractiveContext.Default), contents, pass, file, name); + } + + private static Task TestAsync(InteractiveContext? context, string contents, LintPass pass, string file, string name) + { + var syntax = SyntaxTree.Parse( + new StringSourceText($"{name}.cel", contents), + context != null ? SyntaxMode.Interactive : SyntaxMode.Module, + discardText: true); + var semantics = SemanticTree.Analyze(syntax, context); var analysis = LintAnalysis.Create(semantics, new[] { pass }, LintConfiguration.Default); return VerifyDiagnosticsAsync( diff --git a/src/tests/SemanticTests.cs b/src/tests/SemanticTests.cs index b765978f..161fd825 100644 --- a/src/tests/SemanticTests.cs +++ b/src/tests/SemanticTests.cs @@ -6,10 +6,27 @@ public sealed partial class SemanticTests : CelerityTests { private static Task TestAsync( - SyntaxMode mode, string contents, [CallerFilePath] string file = "", [CallerMemberName] string name = "") + string contents, [CallerFilePath] string file = "", [CallerMemberName] string name = "") { - var syntax = SyntaxTree.Parse(new StringSourceText($"{name}.cel", contents), mode, discardText: true); - var semantics = SemanticTree.Analyze(syntax); + return TestAsync(default(InteractiveContext), contents, file, name); + } + + private static Task TestAsync( + Func contextSetup, + string contents, + [CallerFilePath] string file = "", + [CallerMemberName] string name = "") + { + return TestAsync(contextSetup(InteractiveContext.Default), contents, file, name); + } + + private static Task TestAsync(InteractiveContext? context, string contents, string file, string name) + { + var syntax = SyntaxTree.Parse( + new StringSourceText($"{name}.cel", contents), + context != null ? SyntaxMode.Interactive : SyntaxMode.Module, + discardText: true); + var semantics = SemanticTree.Analyze(syntax, context); return VerifyDiagnosticsAsync(syntax.Diagnostics.Concat(semantics.Diagnostics), file, name); } diff --git a/src/tests/quality/explicitly_undocumented_module.cs b/src/tests/quality/explicitly_undocumented_module.cs index 0fb563a3..566c46d6 100644 --- a/src/tests/quality/explicitly_undocumented_module.cs +++ b/src/tests/quality/explicitly_undocumented_module.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task explicitly_undocumented_module() { return TestAsync( - SyntaxMode.Module, """ @doc false mod { diff --git a/src/tests/quality/lint_severity_attributes.cs b/src/tests/quality/lint_severity_attributes.cs index 5f8560fe..8921b858 100644 --- a/src/tests/quality/lint_severity_attributes.cs +++ b/src/tests/quality/lint_severity_attributes.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task lint_severity_attributes() { return TestAsync( - SyntaxMode.Module, """ mod { fn code() { diff --git a/src/tests/quality/tests_without_assert.cs b/src/tests/quality/tests_without_assert.cs index b8d23e4b..402e3b8e 100644 --- a/src/tests/quality/tests_without_assert.cs +++ b/src/tests/quality/tests_without_assert.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task tests_without_assert() { return TestAsync( - SyntaxMode.Module, """ mod { test foo { diff --git a/src/tests/quality/undocumented_declarations.cs b/src/tests/quality/undocumented_declarations.cs index 2a4558b7..b736e49e 100644 --- a/src/tests/quality/undocumented_declarations.cs +++ b/src/tests/quality/undocumented_declarations.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task undocumented_declarations() { return TestAsync( - SyntaxMode.Module, """ @doc "my fancy module" mod { diff --git a/src/tests/quality/undocumented_module.cs b/src/tests/quality/undocumented_module.cs index a86da0d2..000c45d3 100644 --- a/src/tests/quality/undocumented_module.cs +++ b/src/tests/quality/undocumented_module.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task undocumented_module() { return TestAsync( - SyntaxMode.Module, """ mod { } diff --git a/src/tests/quality/unreachable_code.cs b/src/tests/quality/unreachable_code.cs index c0adc60f..8beaaa7b 100644 --- a/src/tests/quality/unreachable_code.cs +++ b/src/tests/quality/unreachable_code.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task unreachable_code() { return TestAsync( - SyntaxMode.Module, """ mod { err fn main() { diff --git a/src/tests/quality/unreachable_code_submission.cs b/src/tests/quality/unreachable_code_submission.cs new file mode 100644 index 00000000..64269cb3 --- /dev/null +++ b/src/tests/quality/unreachable_code_submission.cs @@ -0,0 +1,16 @@ +public sealed partial class QualityTests +{ + [Fact] + public Task unreachable_code_submission() + { + return TestAsync( + ctx => ctx, + """ + { + ret nil; + 42; + }; + """, + UnreachableCodePass.Instance); + } +} diff --git a/src/tests/quality/unreachable_code_submission.diags.verified.txt b/src/tests/quality/unreachable_code_submission.diags.verified.txt new file mode 100644 index 00000000..4532deac --- /dev/null +++ b/src/tests/quality/unreachable_code_submission.diags.verified.txt @@ -0,0 +1,7 @@ +Warning[unreachable-code]: Code is unreachable +--> unreachable_code_submission.cel (3,5)-(3,8) +1 | { +2 | ret nil; +3 | 42; + : ^^^ +4 | }; diff --git a/src/tests/quality/unused_local_symbols.cs b/src/tests/quality/unused_local_symbols.cs index 127d5caf..ebe73588 100644 --- a/src/tests/quality/unused_local_symbols.cs +++ b/src/tests/quality/unused_local_symbols.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task unused_local_symbols() { return TestAsync( - SyntaxMode.Module, """ mod { const unused_const = 1; diff --git a/src/tests/quality/uppercase_base_indicators.cs b/src/tests/quality/uppercase_base_indicators.cs index 84c7ff79..6cbb847a 100644 --- a/src/tests/quality/uppercase_base_indicators.cs +++ b/src/tests/quality/uppercase_base_indicators.cs @@ -4,7 +4,6 @@ public sealed partial class QualityTests public Task uppercase_base_indicators() { return TestAsync( - SyntaxMode.Module, """ mod { fn foo() { diff --git a/src/tests/semantics/assignment_targets.cs b/src/tests/semantics/assignment_targets.cs index 234a52e7..cade99e5 100644 --- a/src/tests/semantics/assignment_targets.cs +++ b/src/tests/semantics/assignment_targets.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task assignment_targets() { return TestAsync( - SyntaxMode.Module, """ mod { fn assign(arg) { diff --git a/src/tests/semantics/contextual_submissions.cs b/src/tests/semantics/contextual_submissions.cs new file mode 100644 index 00000000..cbc670cc --- /dev/null +++ b/src/tests/semantics/contextual_submissions.cs @@ -0,0 +1,18 @@ +public sealed partial class SemanticTests +{ + [Fact] + public Task contextual_submissions() + { + return TestAsync( + ctx => ctx + .AddSymbol("mutable", mutable: true) + .AddSymbol("immutable", mutable: false), + """ + mutable = 1; + immutable = 2; + let mut immutable = 3; + immutable = 4; + nonexistent = 5; + """); + } +} diff --git a/src/tests/semantics/contextual_submissions.diags.verified.txt b/src/tests/semantics/contextual_submissions.diags.verified.txt new file mode 100644 index 00000000..2ba40e6f --- /dev/null +++ b/src/tests/semantics/contextual_submissions.diags.verified.txt @@ -0,0 +1,15 @@ +Error[E0033]: Assignment to immutable symbol 'immutable' +--> contextual_submissions.cel (2,1)-(2,10) +1 | mutable = 1; +2 | immutable = 2; + : ^^^^^^^^^ +3 | let mut immutable = 3; +4 | immutable = 4; +5 | nonexistent = 5; +Error[E0030]: Unknown symbol name 'nonexistent' +--> contextual_submissions.cel (5,1)-(5,12) +2 | immutable = 2; +3 | let mut immutable = 3; +4 | immutable = 4; +5 | nonexistent = 5; + : ^^^^^^^^^^^ diff --git a/src/tests/semantics/duplicate_fields.cs b/src/tests/semantics/duplicate_fields.cs index 67fff4e9..c9fcf7ad 100644 --- a/src/tests/semantics/duplicate_fields.cs +++ b/src/tests/semantics/duplicate_fields.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task duplicate_fields() { return TestAsync( - SyntaxMode.Module, """ mod { fn foo() { diff --git a/src/tests/semantics/duplicate_symbols.cs b/src/tests/semantics/duplicate_symbols.cs index e14be078..6121720a 100644 --- a/src/tests/semantics/duplicate_symbols.cs +++ b/src/tests/semantics/duplicate_symbols.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task duplicate_symbols() { return TestAsync( - SyntaxMode.Module, """ mod { use Aaa = Bbb; diff --git a/src/tests/semantics/error_handling.cs b/src/tests/semantics/error_handling.cs index 0ed9bc71..0ce62429 100644 --- a/src/tests/semantics/error_handling.cs +++ b/src/tests/semantics/error_handling.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task error_handling() { return TestAsync( - SyntaxMode.Module, """ mod { fn infallible() { diff --git a/src/tests/semantics/let_bindings.cs b/src/tests/semantics/let_bindings.cs index f1f24385..9d7e3663 100644 --- a/src/tests/semantics/let_bindings.cs +++ b/src/tests/semantics/let_bindings.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task let_bindings() { return TestAsync( - SyntaxMode.Module, """ mod { fn bindings(arg) { diff --git a/src/tests/semantics/let_for_binding_order.cs b/src/tests/semantics/let_for_binding_order.cs index ec389d64..05f63100 100644 --- a/src/tests/semantics/let_for_binding_order.cs +++ b/src/tests/semantics/let_for_binding_order.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task let_for_binding_order() { return TestAsync( - SyntaxMode.Module, """ mod { fn foo() { diff --git a/src/tests/semantics/loop_control.cs b/src/tests/semantics/loop_control.cs index b5678865..273ac5df 100644 --- a/src/tests/semantics/loop_control.cs +++ b/src/tests/semantics/loop_control.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task loop_control() { return TestAsync( - SyntaxMode.Module, """ mod { fn loops() { diff --git a/src/tests/semantics/standard_attributes.cs b/src/tests/semantics/standard_attributes.cs index b09a2c77..0e2a83fb 100644 --- a/src/tests/semantics/standard_attributes.cs +++ b/src/tests/semantics/standard_attributes.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task standard_attributes() { return TestAsync( - SyntaxMode.Module, """ @deprecated "broken" @lint "foo:bar" diff --git a/src/tests/semantics/test_reference.cs b/src/tests/semantics/test_reference.cs index 19873a6c..6f35b4b1 100644 --- a/src/tests/semantics/test_reference.cs +++ b/src/tests/semantics/test_reference.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task test_reference() { return TestAsync( - SyntaxMode.Module, """ mod { test foo { assert true; } diff --git a/src/tests/semantics/this_expression.cs b/src/tests/semantics/this_expression.cs index 27af5c43..a315af9b 100644 --- a/src/tests/semantics/this_expression.cs +++ b/src/tests/semantics/this_expression.cs @@ -4,7 +4,6 @@ public sealed partial class SemanticTests public Task this_expression() { return TestAsync( - SyntaxMode.Module, """ mod { fn foo() {