diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index e5051b99..4426cd04 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -3,11 +3,11 @@ name: Links on: pull_request: paths: - - '.github/workflows/links.yml' +# - '.github/workflows/links.yml' Temporarily disabled by @Stargator - 'concept/**/*.md' - 'concept/**/links.json' - 'docs/*.md' - - 'exercises/**/*.md' +# - 'exercises/**/*.md' Temporarily disabled by @Stargator - 'reference/*.md' - '*.md' - '**/**/*.md' diff --git a/config.json b/config.json index 9f148b9f..92bebacf 100644 --- a/config.json +++ b/config.json @@ -153,6 +153,15 @@ "math" ] }, + { + "slug": "forth", + "name": "Forth", + "uuid": "7137cdad-fdac-4e30-836a-c92c352bdc4d", + "practices": [], + "prerequisites": [], + "difficulty": 8, + "topics": ["domain_specific_languages", "parsing", "stacks"] + }, { "slug": "word-count", "name": "Word Count", diff --git a/exercises/practice/forth/.docs/instructions.md b/exercises/practice/forth/.docs/instructions.md new file mode 100644 index 00000000..0368aff8 --- /dev/null +++ b/exercises/practice/forth/.docs/instructions.md @@ -0,0 +1,22 @@ +# Instructions + +Implement an evaluator for a very simple subset of Forth. + +[Forth] is a stack-based programming language. +Implement a very basic evaluator for a small subset of Forth. + +Your evaluator has to support the following words: + +- `+`, `-`, `*`, `/` (integer arithmetic) +- `DUP`, `DROP`, `SWAP`, `OVER` (stack manipulation) + +Your evaluator also has to support defining new words using the customary syntax: `: word-name definition ;`. + +To keep things simple the only data type you need to support is signed integers of at least 16 bits size. + +You should use the following rules for the syntax: a number is a sequence of one or more (ASCII) digits, a word is a sequence of one or more letters, digits, symbols or punctuation that is not a number. +(Forth probably uses slightly different rules, but this is close enough.) + +Words are case-insensitive. + +[Forth]: https://en.wikipedia.org/wiki/Forth_(programming_language) diff --git a/exercises/practice/forth/.meta/config.json b/exercises/practice/forth/.meta/config.json new file mode 100644 index 00000000..e837d879 --- /dev/null +++ b/exercises/practice/forth/.meta/config.json @@ -0,0 +1,22 @@ +{ + "blurb": "Implement an evaluator for a very simple subset of Forth.", + "authors": [ + "matthewmorgan" + ], + "contributors": [ + "AlexeyBukin" + ], + "files": { + "solution": [ + "lib/forth.dart" + ], + "test": [ + "test/forth_test.dart" + ], + "example": [ + ".meta/lib/example.dart" + ] + }, + "source": "Exercism JS track", + "source_url": "https://exercism.org/tracks/javascript/exercises/forth" +} diff --git a/exercises/practice/forth/.meta/lib/example.dart b/exercises/practice/forth/.meta/lib/example.dart new file mode 100644 index 00000000..ac61caef --- /dev/null +++ b/exercises/practice/forth/.meta/lib/example.dart @@ -0,0 +1,150 @@ +import 'dart:collection'; + +class Forth { + final _stack = _Stack(); + + List get stack => _stack.asList; + + late final flattenDictionary = {}; + + late final evaluateDictionary = { + '+': addition, + '-': subtraction, + '*': multiplication, + '/': division, + 'dup': dup, + 'drop': drop, + 'swap': swap, + 'over': over, + }; + + void evaluate(String expression) { + final flat = flatten(expression.toLowerCase()); + + if (flat.isEmpty) { + return; + } + final words = flat.split(' '); + for (final word in words) { + if (evaluateDictionary.containsKey(word)) { + evaluateDictionary[word]!.call(); + } else { + final number = int.parse(word); + _stack.push(number); + } + } + } + + String flatten(String expression) { + final words = expression.split(' '); + for (int t = 0; t < words.length; t++) { + final word = words[t]; + if (flattenDictionary.containsKey(word)) { + words[t] = flattenDictionary[word]!; + } else if (word == ':') { + final semicolon = words.indexOf(';', t); + if (semicolon == -1) { + throw Exception('Unterminated definition'); + } + defineCommand(words[t + 1], words.sublist(t + 2, semicolon).join(' ')); + words.removeRange(t, semicolon + 1); + t = semicolon; + } else if (!evaluateDictionary.containsKey(word) && !isKeyword(word)) { + throw Exception('Unknown command'); + } + } + return words.join(' '); + } + + /// Must be not a number (-?\d+), not a ':', not a ';' + static bool isKeyword(String word) => RegExp(r'^(-?\d+|:|;)$').hasMatch(word); + + void defineCommand(String word, String expression) { + if (isKeyword(word)) { + throw Exception('Invalid definition'); + } + flattenDictionary[word] = flatten(expression); + } + + void addition() { + final a = _stack.pop(); + final b = _stack.pop(); + _stack.push(b + a); + } + + void subtraction() { + final a = _stack.pop(); + final b = _stack.pop(); + _stack.push(b - a); + } + + void multiplication() { + final a = _stack.pop(); + final b = _stack.pop(); + _stack.push(b * a); + } + + void division() { + final a = _stack.pop(); + if (a == 0) { + throw Exception('Division by zero'); + } + final b = _stack.pop(); + _stack.push(b ~/ a); + } + + void dup() { + final a = _stack.peek(); + _stack.push(a); + } + + void drop() { + _stack.pop(); + } + + void swap() { + final a = _stack.pop(); + final b = _stack.pop(); + _stack.push(a); + _stack.push(b); + } + + void over() { + final a = _stack.pop(); + final b = _stack.peek(); + _stack.push(a); + _stack.push(b); + } +} + +class _Stack { + List get asList => _stack.toList(); + + final _stack = Queue(); + + bool get isEmpty => _stack.isEmpty; + + int get size => _stack.length; + + int peek() { + if (_stack.isEmpty) { + throw Exception('Stack empty'); + } + return _stack.last; + } + + int pop() { + if (_stack.isEmpty) { + throw Exception('Stack empty'); + } + return _stack.removeLast(); + } + + void push(int number) { + _stack.addLast(number); + } + + void deleteAll({int after = 0}) { + _stack.take(after); + } +} diff --git a/exercises/practice/forth/analysis_options.yaml b/exercises/practice/forth/analysis_options.yaml new file mode 100644 index 00000000..c06363d6 --- /dev/null +++ b/exercises/practice/forth/analysis_options.yaml @@ -0,0 +1,18 @@ +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + unused_element: error + unused_import: error + unused_local_variable: error + dead_code: error + +linter: + rules: + # Error Rules + - avoid_relative_lib_imports + - avoid_types_as_parameter_names + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - valid_regexps diff --git a/exercises/practice/forth/lib/forth.dart b/exercises/practice/forth/lib/forth.dart new file mode 100644 index 00000000..dbb3b297 --- /dev/null +++ b/exercises/practice/forth/lib/forth.dart @@ -0,0 +1,3 @@ +class Forth { + // Put your code here +} diff --git a/exercises/practice/forth/pubspec.yaml b/exercises/practice/forth/pubspec.yaml new file mode 100644 index 00000000..764fefc9 --- /dev/null +++ b/exercises/practice/forth/pubspec.yaml @@ -0,0 +1,5 @@ +name: 'forth' +environment: + sdk: '>=2.12.0 <3.0.0' +dev_dependencies: + test: '<2.0.0' diff --git a/exercises/practice/forth/test/forth_test.dart b/exercises/practice/forth/test/forth_test.dart new file mode 100644 index 00000000..258b4bef --- /dev/null +++ b/exercises/practice/forth/test/forth_test.dart @@ -0,0 +1,295 @@ +import 'package:forth/forth.dart'; +import 'package:test/test.dart'; + +void main() { + group("Forth", forthTests); +} + +void forthTests() { + final throwsEmptyStack = throwsA(isA().having((e) => e.toString(), 'message', 'Exception: Stack empty')); + + late Forth forth; + + setUp(() => forth = Forth()); + + group('parsing and numbers', () { + test('numbers just get pushed onto the stack', () { + forth.evaluate('1 2 3 4 5'); + expect(forth.stack, equals([1, 2, 3, 4, 5])); + }); + + test('pushes negative numbers onto the stack', () { + forth.evaluate('-1 -2 -3 -4 -5'); + expect(forth.stack, equals([-1, -2, -3, -4, -5])); + }); + }); + + group('addition', () { + test('can add two numbers', () { + forth.evaluate('1 2 +'); + expect(forth.stack, equals([3])); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('+'), throwsEmptyStack); + }); + + test('errors if there is only one value on the stack', () { + expect(() => forth.evaluate('1 +'), throwsEmptyStack); + }); + }); + + group('subtraction', () { + test('can subtract two numbers', () { + forth.evaluate('3 4 -'); + expect(forth.stack, equals([-1])); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('-'), throwsEmptyStack); + }); + + test('errors if there is only one value on the stack', () { + expect(() => forth.evaluate('1 -'), throwsEmptyStack); + }); + }); + + group('multiplication', () { + test('can multiply two numbers', () { + forth.evaluate('2 4 *'); + expect(forth.stack, equals([8])); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('*'), throwsEmptyStack); + }); + + test('errors if there is only one value on the stack', () { + expect(() => forth.evaluate('1 *'), throwsEmptyStack); + }); + }); + + group('division', () { + test('can divide two numbers', () { + forth.evaluate('12 3 /'); + expect(forth.stack, equals([4])); + }); + + test('performs integer division', () { + forth.evaluate('8 3 /'); + expect(forth.stack, equals([2])); + }); + + test('errors if dividing by zero', () { + expect( + () => forth.evaluate('4 0 /'), + throwsA(isA().having((e) => e.toString(), 'message', 'Exception: Division by zero')), + ); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('/'), throwsEmptyStack); + }); + + test('errors if there is only one value on the stack', () { + expect(() => forth.evaluate('1 /'), throwsEmptyStack); + }); + }); + + group('combined arithmetic', () { + test('addition and subtraction', () { + forth.evaluate('1 2 + 4 -'); + expect(forth.stack, equals([-1])); + }); + + test('multiplication and division', () { + forth.evaluate('2 4 * 3 /'); + expect(forth.stack, equals([2])); + }); + }); + + group('dup', () { + test('copies a value on the stack', () { + forth.evaluate('1 dup'); + expect(forth.stack, equals([1, 1])); + }); + + test('copies the top value on the stack', () { + forth.evaluate('1 2 dup'); + expect(forth.stack, equals([1, 2, 2])); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('dup'), throwsEmptyStack); + }); + }); + + group('drop', () { + test('removes the top value on the stack if it is the only one', () { + forth.evaluate('1 drop'); + expect(forth.stack, equals([])); + }); + + test('removes the top value on the stack if it is not the only one', () { + forth.evaluate('1 2 drop'); + expect(forth.stack, equals([1])); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('drop'), throwsEmptyStack); + }); + }); + + group('swap', () { + test('swaps the top two values on the stack if they are the only ones', () { + forth.evaluate('1 2 swap'); + expect(forth.stack, equals([2, 1])); + }); + + test('swaps the top two values on the stack if they are not the only ones', () { + forth.evaluate('1 2 3 swap'); + expect(forth.stack, equals([1, 3, 2])); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('swap'), throwsEmptyStack); + }); + + test('errors if there is only one value on the stack', () { + expect(() => forth.evaluate('1 swap'), throwsEmptyStack); + }); + }); + + group('over', () { + test('copies the second element if there are only two', () { + forth.evaluate('1 2 over'); + expect(forth.stack, equals([1, 2, 1])); + }); + + test('copies the second element if there are more than two', () { + forth.evaluate('1 2 3 over'); + expect(forth.stack, equals([1, 2, 3, 2])); + }); + + test('errors if there is nothing on the stack', () { + expect(() => forth.evaluate('over'), throwsEmptyStack); + }); + + test('errors if there is only one value on the stack', () { + expect(() => forth.evaluate('1 over'), throwsEmptyStack); + }); + }); + + group('user-defined words', () { + test('can consist of built-in words', () { + forth.evaluate(': dup-twice dup dup ;'); + forth.evaluate('1 dup-twice'); + expect(forth.stack, equals([1, 1, 1])); + }); + + test('execute in the right order', () { + forth.evaluate(': countup 1 2 3 ;'); + forth.evaluate('countup'); + expect(forth.stack, equals([1, 2, 3])); + }); + + test('can override other user-defined words', () { + forth.evaluate(': foo dup ;'); + forth.evaluate(': foo dup dup ;'); + forth.evaluate('1 foo'); + expect(forth.stack, equals([1, 1, 1])); + }); + + test('can override built-in words', () { + forth.evaluate(': swap dup ;'); + forth.evaluate('1 swap'); + expect(forth.stack, equals([1, 1])); + }); + + test('can override built-in operators', () { + forth.evaluate(': + * ;'); + forth.evaluate('3 4 +'); + expect(forth.stack, equals([12])); + }); + + test('can use different words with the same name', () { + forth.evaluate(': foo 5 ;'); + forth.evaluate(': bar foo ;'); + forth.evaluate(': foo 6 ;'); + forth.evaluate('bar foo'); + expect(forth.stack, equals([5, 6])); + }); + + test('can define word that uses word with the same name', () { + forth.evaluate(': foo 10 ;'); + forth.evaluate(': foo foo 1 + ;'); + forth.evaluate('foo'); + expect(forth.stack, equals([11])); + }); + + test('cannot redefine numbers', () { + expect( + () => forth.evaluate(': 1 2 ;'), + throwsA(isA().having((e) => e.toString(), 'message', 'Exception: Invalid definition')), + ); + }); + + test('cannot redefine negative numbers', () { + expect( + () => forth.evaluate(': -1 2 ;'), + throwsA(isA().having((e) => e.toString(), 'message', 'Exception: Invalid definition')), + ); + }); + + test('errors if executing a non-existent word', () { + expect( + () => forth.evaluate('foo'), + throwsA(isA().having((e) => e.toString(), 'message', 'Exception: Unknown command')), + ); + }); + + test('only defines locally', () { + final first = Forth(); + final second = Forth(); + first.evaluate(': + - ;'); + first.evaluate('1 1 +'); + second.evaluate('1 1 +'); + expect(first.stack, equals([0])); + expect(second.stack, equals([2])); + }); + }); + + group('case-insensitivity', () { + test('DUP is case-insensitive', () { + forth.evaluate('1 DUP Dup dup'); + expect(forth.stack, equals([1, 1, 1, 1])); + }); + + test('DROP is case-insensitive', () { + forth.evaluate('1 2 3 4 DROP Drop drop'); + expect(forth.stack, equals([1])); + }); + + test('SWAP is case-insensitive', () { + forth.evaluate('1 2 SWAP 3 Swap 4 swap'); + expect(forth.stack, equals([2, 3, 4, 1])); + }); + + test('OVER is case-insensitive', () { + forth.evaluate('1 2 OVER Over over'); + expect(forth.stack, equals([1, 2, 1, 2, 1])); + }); + + test('user-defined words are case-insensitive', () { + forth.evaluate(': foo dup ;'); + forth.evaluate('1 FOO Foo foo'); + expect(forth.stack, equals([1, 1, 1, 1])); + }); + + test('definitions are case-insensitive', () { + forth.evaluate(': SWAP DUP Dup dup ;'); + forth.evaluate('1 swap'); + expect(forth.stack, equals([1, 1, 1, 1])); + }); + }); +}