├── .ruby-version ├── .rspec ├── peacock ├── peacock-formatter ├── .yarnrc ├── todo ├── .vscodeignore ├── CHANGELOG.md ├── src │ ├── test │ │ ├── suite │ │ │ ├── extension.test.ts │ │ │ └── index.ts │ │ └── runTest.ts │ └── extension.ts ├── .eslintrc.json ├── tsconfig.json ├── package.json ├── webpack.config.js ├── README.md └── vsc-extension-quickstart.md ├── main.pea ├── spec ├── codegen │ ├── misc │ │ ├── not.pea │ │ ├── dot_operator.pea │ │ ├── simple_assignment.pea │ │ ├── simple_float.pea │ │ ├── bind_2_args.pea │ │ ├── mod.pea │ │ ├── self_lookup.pea │ │ ├── simple_array.pea │ │ ├── call_zip.pea │ │ ├── optional_chain.pea │ │ ├── bind_operator.pea │ │ ├── bind_with_args.pea │ │ └── in_and_eq.pea │ ├── assignment │ │ ├── reassign.pea │ │ └── array_deconstruct.pea │ ├── object_literals │ │ ├── spread.pea │ │ ├── object_literal.pea │ │ ├── object_arrow_method.pea │ │ └── object_entry_multiline_function.pea │ ├── functions │ │ ├── function_call.pea │ │ ├── short_fn.pea │ │ ├── no_arg_shorthand.pea │ │ ├── oneline_function_no_args.pea │ │ ├── arrow_function_without_parens.pea │ │ ├── oneline_add.pea │ │ ├── multiline_add.pea │ │ └── arrow_function.pea │ ├── array │ │ ├── spread.pea │ │ ├── append.pea │ │ └── group_by.pea │ ├── schemas │ │ ├── schema_definition.pea │ │ ├── schema_object_int.pea │ │ ├── schema_union.pea │ │ ├── short_fn.pea │ │ ├── null_fn_pattern.pea │ │ ├── intersection.pea │ │ ├── iterate_union.pea │ │ ├── schema_assignment.pea │ │ ├── schema_named_captures.pea │ │ ├── int_schema.pea │ │ └── schema_await.pea │ ├── classes │ │ ├── instance_var.pea │ │ ├── getter.pea │ │ ├── parser_constructor.pea │ │ ├── parser.pea │ │ ├── token.pea │ │ └── static_from.pea │ ├── for_comprehension │ │ ├── range.pea │ │ └── if.pea │ ├── range │ │ ├── spread.pea │ │ ├── times.pea │ │ ├── times_1.pea │ │ ├── times_named.pea │ │ └── to_a.pea │ ├── enum │ │ └── enum_1.pea │ ├── for_loop │ │ ├── for_of_loop.pea │ │ ├── for_in.pea │ │ └── deconstruct_for_of.pea │ ├── if │ │ └── basic.pea │ ├── when │ │ └── no_expr.pea │ └── case_function │ │ ├── fib.pea │ │ ├── factorial.pea │ │ └── to_a.pea ├── snapshot_spec.rb ├── lexer_spec.rb └── spec_helper.rb ├── lib ├── pea_std_lib │ ├── std.pea │ ├── string.pea │ ├── array.pea │ ├── range.js │ ├── symbols.js │ ├── schema.js │ └── css_preprocessor.js ├── utils.rb ├── formatter.rb ├── type_checker.rb ├── lexer.rb ├── ast.rb ├── compiler.rb └── parser.rb ├── logo.png ├── Gemfile ├── public └── favicon.png ├── examples ├── todo_mvc │ ├── active │ │ └── index.pea │ ├── complete │ │ └── index.pea │ ├── Form.pea │ ├── Page.pea │ └── List.pea ├── misc │ └── VideoLibrary.pea ├── css_parser │ └── lexer.pea └── parser │ └── parser.pea ├── .gitignore ├── .solargraph.yml ├── format ├── snapshot.rb ├── package.json ├── main.rb ├── Gemfile.lock └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.2 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /peacock: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | to_html $1 -------------------------------------------------------------------------------- /peacock-formatter/.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /main.pea: -------------------------------------------------------------------------------- 1 | enum User = One | Two 2 | 3 | a := User#One -------------------------------------------------------------------------------- /peacock-formatter/todo: -------------------------------------------------------------------------------- 1 | [ ] move cursor back to place -------------------------------------------------------------------------------- /spec/codegen/misc/not.pea: -------------------------------------------------------------------------------- 1 | # !true; 2 | 3 | !true 4 | -------------------------------------------------------------------------------- /lib/pea_std_lib/std.pea: -------------------------------------------------------------------------------- 1 | function to_s = this.toString() 2 | -------------------------------------------------------------------------------- /spec/codegen/assignment/reassign.pea: -------------------------------------------------------------------------------- 1 | # a = 11; 2 | 3 | a = 11 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcellerusu/peacock-lang/HEAD/logo.png -------------------------------------------------------------------------------- /spec/codegen/misc/dot_operator.pea: -------------------------------------------------------------------------------- 1 | # console.log(a.b); 2 | 3 | console.log a.b 4 | -------------------------------------------------------------------------------- /spec/codegen/misc/simple_assignment.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # a = 1; 3 | 4 | a := 1 5 | -------------------------------------------------------------------------------- /spec/codegen/misc/simple_float.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # a = 1.1; 3 | 4 | a := 1.1 5 | -------------------------------------------------------------------------------- /spec/codegen/misc/bind_2_args.pea: -------------------------------------------------------------------------------- 1 | # test.call([1], [2], [1]); 2 | [1]::test [2], [1] 3 | -------------------------------------------------------------------------------- /spec/codegen/misc/mod.pea: -------------------------------------------------------------------------------- 1 | # console.log(31 % 10); 2 | 3 | console.log 31 mod 10 4 | -------------------------------------------------------------------------------- /spec/codegen/misc/self_lookup.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # a = a + 10; 3 | 4 | a := a + 10 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "rspec", "~> 3.10" 3 | gem "solargraph" 4 | -------------------------------------------------------------------------------- /spec/codegen/misc/simple_array.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # a = [1, "20"]; 3 | 4 | a := [1, "20"] 5 | -------------------------------------------------------------------------------- /spec/codegen/object_literals/spread.pea: -------------------------------------------------------------------------------- 1 | # let o1; 2 | # o1 = { ...o }; 3 | o1 := { ...o } 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcellerusu/peacock-lang/HEAD/public/favicon.png -------------------------------------------------------------------------------- /spec/codegen/functions/function_call.pea: -------------------------------------------------------------------------------- 1 | # add(1, "10", [1, 2]); 2 | 3 | add(1, "10", [1, 2]) 4 | -------------------------------------------------------------------------------- /spec/codegen/array/spread.pea: -------------------------------------------------------------------------------- 1 | # console.log([...[1, 2, 3]]); 2 | 3 | console.log [...[1, 2, 3]] 4 | -------------------------------------------------------------------------------- /spec/codegen/misc/call_zip.pea: -------------------------------------------------------------------------------- 1 | # console.log(zip.call([1], [2])); 2 | 3 | console.log [1]::zip [2] 4 | -------------------------------------------------------------------------------- /spec/codegen/schemas/schema_definition.pea: -------------------------------------------------------------------------------- 1 | # const User = { id: s('id') }; 2 | 3 | schema User = { id } 4 | -------------------------------------------------------------------------------- /spec/codegen/schemas/schema_object_int.pea: -------------------------------------------------------------------------------- 1 | # const User = { id: 10 }; 2 | 3 | schema User = { id: 10 } 4 | -------------------------------------------------------------------------------- /spec/codegen/schemas/schema_union.pea: -------------------------------------------------------------------------------- 1 | # const Nums = s.union(1, 2, 3); 2 | 3 | schema Nums = 1 | 2 | 3 4 | -------------------------------------------------------------------------------- /spec/codegen/functions/short_fn.pea: -------------------------------------------------------------------------------- 1 | # let double; 2 | # double = (_it => _it * 2); 3 | 4 | double := #{ % * 2 } 5 | -------------------------------------------------------------------------------- /spec/codegen/object_literals/object_literal.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # a = { b: 10 }; 3 | 4 | a := { 5 | b: 10 6 | } 7 | -------------------------------------------------------------------------------- /spec/codegen/functions/no_arg_shorthand.pea: -------------------------------------------------------------------------------- 1 | # function ten() { 2 | # return 10; 3 | # }; 4 | 5 | function ten = 10 6 | -------------------------------------------------------------------------------- /spec/codegen/schemas/short_fn.pea: -------------------------------------------------------------------------------- 1 | # const User = { id: (_it => _it > 10) }; 2 | 3 | schema User = { id: #{ % > 10 } } 4 | -------------------------------------------------------------------------------- /spec/codegen/functions/oneline_function_no_args.pea: -------------------------------------------------------------------------------- 1 | # function a() { 2 | # return 10; 3 | # }; 4 | 5 | function a() = 10 6 | -------------------------------------------------------------------------------- /spec/codegen/classes/instance_var.pea: -------------------------------------------------------------------------------- 1 | # class Parser { 2 | # body = []; 3 | # }; 4 | 5 | class Parser 6 | body := [] 7 | end 8 | -------------------------------------------------------------------------------- /spec/codegen/functions/arrow_function_without_parens.pea: -------------------------------------------------------------------------------- 1 | # let double; 2 | # double = (x) => x * 2; 3 | 4 | double := x => x * 2 5 | -------------------------------------------------------------------------------- /spec/codegen/functions/oneline_add.pea: -------------------------------------------------------------------------------- 1 | # function add(a, b) { 2 | # return a + b; 3 | # }; 4 | 5 | function add(a, b) = a + b 6 | -------------------------------------------------------------------------------- /spec/codegen/misc/optional_chain.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # a = {}; 3 | # console.log(a?.b); 4 | 5 | a := {} 6 | 7 | console.log a?.b 8 | -------------------------------------------------------------------------------- /spec/codegen/functions/multiline_add.pea: -------------------------------------------------------------------------------- 1 | # function add(a, b) { 2 | # return a + b; 3 | # }; 4 | 5 | function add(a, b) 6 | a + b 7 | end 8 | -------------------------------------------------------------------------------- /spec/codegen/for_comprehension/range.pea: -------------------------------------------------------------------------------- 1 | # console.log(Array.from(new Range(0, 10), num => num * 10)); 2 | 3 | console.log [num * 10 for num in 0..10] 4 | -------------------------------------------------------------------------------- /spec/codegen/assignment/array_deconstruct.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # let _temp = [1]; 3 | # s.verify([s('a')], _temp, 'Array'); 4 | # [a] = _temp; 5 | 6 | [a] := [1] 7 | -------------------------------------------------------------------------------- /spec/codegen/schemas/null_fn_pattern.pea: -------------------------------------------------------------------------------- 1 | # function a(...args) { 2 | # s.verify(null, args[0], 'null') 3 | # return 1; 4 | # }; 5 | 6 | function a(null) = 1 7 | -------------------------------------------------------------------------------- /spec/codegen/range/spread.pea: -------------------------------------------------------------------------------- 1 | # console.log([...new Range("a", "z")]); 2 | # console.log([...new Range(0, 10)]); 3 | 4 | console.log [..."a".."z"] 5 | console.log [...0..10] 6 | -------------------------------------------------------------------------------- /examples/todo_mvc/active/index.pea: -------------------------------------------------------------------------------- 1 | import List from "../List" 2 | 3 | component { items } 4 | items := items.filter(item => !item.completed) 5 | in 6 | 7 | end 8 | -------------------------------------------------------------------------------- /examples/todo_mvc/complete/index.pea: -------------------------------------------------------------------------------- 1 | import List from "../List" 2 | 3 | component { items } 4 | items := items.filter(item => !item.completed) 5 | in 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/codegen/for_comprehension/if.pea: -------------------------------------------------------------------------------- 1 | # console.log(Array.from(new Range(0, 10).filter(num => num > 3), num => num * 10)); 2 | 3 | console.log [num * 10 for num in 0..10 if num > 3] 4 | -------------------------------------------------------------------------------- /spec/codegen/functions/arrow_function.pea: -------------------------------------------------------------------------------- 1 | # let double; 2 | # double = (() => { return 10 * 2; }); 3 | # console.log(double()); 4 | 5 | double := () => 10 * 2 6 | console.log double() 7 | -------------------------------------------------------------------------------- /spec/codegen/enum/enum_1.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # const User = { 3 | # One: Symbol('One'), 4 | # Two: Symbol('Two'), 5 | # }; 6 | # a = User.One; 7 | 8 | 9 | enum User = One | Two 10 | 11 | a := User#One -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output.html 2 | bin 3 | alltogether.rb 4 | alltogether.rb.js 5 | .vscode 6 | .bundle 7 | node_modules 8 | yarn.lock 9 | index.html 10 | rspec*.pea 11 | test.js 12 | .DS_Store 13 | dist 14 | output.js -------------------------------------------------------------------------------- /spec/codegen/object_literals/object_arrow_method.pea: -------------------------------------------------------------------------------- 1 | # let PMath; 2 | # PMath = { 3 | # add(a, b) { 4 | # return a + b; 5 | # }, 6 | # }; 7 | 8 | PMath := { 9 | add(a, b) => a + b, 10 | } 11 | -------------------------------------------------------------------------------- /spec/codegen/schemas/intersection.pea: -------------------------------------------------------------------------------- 1 | # const User = { id: s('id'), admin: s('admin') }; 2 | # const Admin = { ...User, admin: true }; 3 | 4 | schema User = { id, admin } 5 | schema Admin = User & { admin: true } 6 | -------------------------------------------------------------------------------- /spec/codegen/schemas/iterate_union.pea: -------------------------------------------------------------------------------- 1 | # const A = s.union(1, 2, 3); 2 | # for (let a of A) { 3 | # console.log(a); 4 | # }; 5 | 6 | schema A = 1 | 2 | 3 7 | 8 | for a of A 9 | console.log a 10 | end 11 | -------------------------------------------------------------------------------- /spec/codegen/classes/getter.pea: -------------------------------------------------------------------------------- 1 | # class Parser { 2 | # get current_token() { 3 | # return this.tokens[this.pos]; 4 | # } 5 | # }; 6 | 7 | class Parser 8 | get current_token = this.tokens[this.pos] 9 | end 10 | -------------------------------------------------------------------------------- /spec/codegen/for_loop/for_of_loop.pea: -------------------------------------------------------------------------------- 1 | # let arr; 2 | # arr = [1, 2, 3]; 3 | # for (let elem of arr) { 4 | # console.log(elem); 5 | # }; 6 | 7 | arr := [1, 2, 3] 8 | 9 | for elem of arr 10 | console.log elem 11 | end 12 | -------------------------------------------------------------------------------- /spec/codegen/misc/bind_operator.pea: -------------------------------------------------------------------------------- 1 | # function length() { 2 | # return this.length; 3 | # }; 4 | # console.log(length.call("abc")); 5 | 6 | function length 7 | this.length 8 | end 9 | 10 | console.log "abc"::length 11 | -------------------------------------------------------------------------------- /spec/codegen/object_literals/object_entry_multiline_function.pea: -------------------------------------------------------------------------------- 1 | # let PMath; 2 | # PMath = { 3 | # add(a, b) { 4 | # return a + b; 5 | # } 6 | # }; 7 | 8 | PMath := { 9 | function add(a, b) 10 | a + b 11 | end 12 | } 13 | -------------------------------------------------------------------------------- /spec/codegen/classes/parser_constructor.pea: -------------------------------------------------------------------------------- 1 | # class Parser { 2 | # constructor(name) { 3 | # this.name = name; 4 | # } 5 | # }; 6 | 7 | class Parser 8 | function constructor(name) 9 | this.name := name 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/codegen/misc/bind_with_args.pea: -------------------------------------------------------------------------------- 1 | # function length() { 2 | # return this.length; 3 | # }; 4 | # console.log(length.call("abc", 10, 20)); 5 | 6 | function length 7 | this.length 8 | end 9 | 10 | console.log "abc"::length(10, 20) 11 | -------------------------------------------------------------------------------- /spec/codegen/for_loop/for_in.pea: -------------------------------------------------------------------------------- 1 | # let obj; 2 | # obj = { a: 10, b: "str" }; 3 | # for (let key in obj) { 4 | # console.log(key); 5 | # }; 6 | 7 | obj := { 8 | a: 10, 9 | b: "str" 10 | } 11 | 12 | for key in obj 13 | console.log key 14 | end 15 | -------------------------------------------------------------------------------- /peacock-formatter/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | -------------------------------------------------------------------------------- /spec/codegen/for_loop/deconstruct_for_of.pea: -------------------------------------------------------------------------------- 1 | # let arr; 2 | # arr = [{ num: 10 }, { num: 11 }]; 3 | # for (let { num } of arr) { 4 | # console.log(num); 5 | # }; 6 | 7 | arr := [{ num: 10 }, { num: 11 }] 8 | 9 | for { num } of arr 10 | console.log(num) 11 | end 12 | -------------------------------------------------------------------------------- /spec/codegen/schemas/schema_assignment.pea: -------------------------------------------------------------------------------- 1 | # let user; 2 | # const User = { id: s('id') }; 3 | # user = { id: 10 }; 4 | # user = s.verify(User, user, 'User'); 5 | # console.log(user); 6 | 7 | schema User = { id } 8 | 9 | user := { id: 10 } 10 | User(user) := user 11 | console.log user 12 | -------------------------------------------------------------------------------- /peacock-formatter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "peacock-formatter" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /spec/codegen/if/basic.pea: -------------------------------------------------------------------------------- 1 | # if (10[Symbol.peacock_equals](11)) { 2 | # console.log("true"); 3 | # } else if (false) { 4 | # 5 | # } else { 6 | # console.log("false"); 7 | # }; 8 | 9 | if 10 == 11 10 | console.log "true" 11 | else if false 12 | 13 | else 14 | console.log "false" 15 | end 16 | -------------------------------------------------------------------------------- /spec/codegen/array/append.pea: -------------------------------------------------------------------------------- 1 | # function append(item) { 2 | # if (!(this instanceof Array)) throw new MatchError('Expected `this` to be a `Array`'); 3 | # return [...this, item]; 4 | # }; 5 | # console.log(append.call([1], 10)); 6 | 7 | function Array::append(item) = [...this, item] 8 | 9 | console.log [1]::append(10) 10 | -------------------------------------------------------------------------------- /spec/codegen/schemas/schema_named_captures.pea: -------------------------------------------------------------------------------- 1 | # let user; 2 | # const User = { id: s('id'), user: s('id') }; 3 | # user = { id: 10, user: 10 }; 4 | # user = s.verify(User, user, 'User'); 5 | # console.log(user); 6 | 7 | schema User = { id, user: :id } 8 | user := { id: 10, user: 10 } 9 | User(user) := user 10 | console.log user 11 | -------------------------------------------------------------------------------- /spec/codegen/range/times.pea: -------------------------------------------------------------------------------- 1 | # function times(callback) { 2 | # for (let item of this) { 3 | # callback(item); 4 | # }; 5 | # }; 6 | # times.call(new Range(0, 100), (_it => console.log(_it))); 7 | 8 | function times(callback) 9 | for item of this 10 | callback item 11 | end 12 | end 13 | 14 | 0..100::times(#{ 15 | console.log % 16 | }) 17 | -------------------------------------------------------------------------------- /spec/codegen/range/times_1.pea: -------------------------------------------------------------------------------- 1 | # function times(callback) { 2 | # for (let item of this) { 3 | # callback(item); 4 | # }; 5 | # }; 6 | # times.call(new Range(0, 100), (_it => console.log(_it))); 7 | 8 | function times(callback) 9 | for item of this 10 | callback item 11 | end 12 | end 13 | 14 | 0..100::times #{ 15 | console.log % 16 | } 17 | -------------------------------------------------------------------------------- /spec/codegen/schemas/int_schema.pea: -------------------------------------------------------------------------------- 1 | # const Ten = 10; 2 | # const Eleven = 11; 3 | # function add(a, b) { 4 | # a = s.verify(Ten, a); 5 | # b = s.verify(Eleven, b); 6 | # return a + b; 7 | # }; 8 | # console.log(add(10, 11)); 9 | 10 | schema Ten = 10 11 | schema Eleven = 11 12 | 13 | function add(Ten(a), Eleven(b)) = a + b 14 | 15 | console.log add(10, 11) 16 | -------------------------------------------------------------------------------- /spec/codegen/range/times_named.pea: -------------------------------------------------------------------------------- 1 | # function times(callback) { 2 | # for (let item of this) { 3 | # callback(item); 4 | # }; 5 | # }; 6 | # times.call(new Range(0, 100), (num => console.log(num))); 7 | 8 | function times(callback) 9 | for item of this 10 | callback item 11 | end 12 | end 13 | 14 | 0..100::times #{ |num| 15 | console.log num 16 | } 17 | -------------------------------------------------------------------------------- /spec/codegen/when/no_expr.pea: -------------------------------------------------------------------------------- 1 | # if (true && false) { 2 | # console.log("true"); 3 | # } else if (false || true && false) { 4 | # console.log("false"); 5 | # } else { 6 | # console.log("else"); 7 | # }; 8 | 9 | case 10 | when true && false 11 | console.log("true") 12 | when false || true && false 13 | console.log("false") 14 | else 15 | console.log("else") 16 | end 17 | -------------------------------------------------------------------------------- /lib/utils.rb: -------------------------------------------------------------------------------- 1 | class AssertionError < RuntimeError 2 | end 3 | 4 | class NotImplemented < AssertionError 5 | end 6 | 7 | def assert(&block) 8 | raise AssertionError unless yield 9 | end 10 | 11 | def assert_not_reached! 12 | raise AssertionError 13 | end 14 | 15 | def not_implemented!(&block) 16 | if block 17 | block.call 18 | end 19 | raise NotImplemented 20 | end 21 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - "**/*.rb" 4 | exclude: 5 | - spec/**/* 6 | - test/**/* 7 | - vendor/**/* 8 | - ".bundle/**/*" 9 | require: [] 10 | domains: [] 11 | reporters: 12 | - rubocop 13 | - require_not_found 14 | formatter: 15 | rubocop: 16 | cops: safe 17 | except: [] 18 | only: [] 19 | extra_args: [] 20 | require_paths: [] 21 | plugins: [] 22 | max_files: 5000 23 | -------------------------------------------------------------------------------- /spec/codegen/range/to_a.pea: -------------------------------------------------------------------------------- 1 | # let a; 2 | # function to_a() { 3 | # return Array.from(this); 4 | # }; 5 | # a = new Range(1, 5); 6 | # console.log(a); 7 | # console.log(to_a.call(a.filter((x) => x > 3))); 8 | # console.log(to_a.call(a)); 9 | 10 | function to_a = Array.from this 11 | 12 | a := 1..5 13 | 14 | console.log a 15 | console.log a.filter(x => x > 3)::to_a 16 | console.log a::to_a 17 | -------------------------------------------------------------------------------- /spec/codegen/misc/in_and_eq.pea: -------------------------------------------------------------------------------- 1 | # let map; 2 | # map = new Map([["a", [1, 2, { a: 11 }]]]); 3 | # console.log(map[Symbol.peacock_contains](["a", [1, 2, { a: 11 }]])); 4 | # console.log(new Map([["a", [1, 2, { a: 11 }]]])[Symbol.peacock_equals](map)); 5 | 6 | map := new Map([ 7 | ["a", [1, 2, { a: 11 }]] 8 | ]) 9 | 10 | console.log ["a", [1, 2, { a: 11 }]] in map 11 | console.log new Map([["a", [1, 2, { a: 11 }]]]) == map 12 | -------------------------------------------------------------------------------- /examples/misc/VideoLibrary.pea: -------------------------------------------------------------------------------- 1 | schema VideoWithViews = Video({ views: #{ it >= 100 } }) 2 | 3 | component { videos } 4 | a := 10 5 | when { videos: [] } 6 |
7 | ..upload a video! 8 |
9 | when { videos: [ VideoWithViews({ views }) ] } 10 |
11 | Congrats on your first video! It reached over {views} 12 |
13 | when videos.length > 10 14 |
15 | 10 Videos! 16 |
17 | else 18 | 19 | end -------------------------------------------------------------------------------- /spec/codegen/schemas/schema_await.pea: -------------------------------------------------------------------------------- 1 | # let request, todo; 2 | # const Todo = { id: s('id'), userId: s('id') }; 3 | # request = await fetch("https://jsonplaceholder.typicode.com/todos/1"); 4 | # todo = s.verify(Todo, await request.json(), 'Todo'); 5 | # console.log(todo); 6 | 7 | schema Todo = { id, userId: :id } 8 | 9 | request := await fetch("https://jsonplaceholder.typicode.com/todos/1") 10 | Todo(todo) := await request.json() 11 | 12 | console.log todo 13 | -------------------------------------------------------------------------------- /lib/pea_std_lib/string.pea: -------------------------------------------------------------------------------- 1 | class StringScanner(@str) 2 | index := 0 3 | matched := null 4 | groups := [] 5 | 6 | get rest_of_str = this.str.slice(this.index) 7 | get is_end_of_str = this.index >= this.str.length 8 | 9 | function scan(regex) 10 | result := this.rest_of_str.match(regex) 11 | return false if result === null || result.index !== 0 12 | [this.matched, ...this.groups] := Array.from(result) 13 | this.index += this.matched.length 14 | return true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/codegen/case_function/fib.pea: -------------------------------------------------------------------------------- 1 | # function fib(...args) { 2 | # if (s.check([0], args)) { 3 | # return 0; 4 | # } else if (s.check([1], args)) { 5 | # return 1; 6 | # } else if (s.check([s('n')], args)) { 7 | # let n = args[0]; 8 | # return fib(n - 1) + fib(n - 2); 9 | # } else throw new MatchError(); 10 | # }; 11 | # console.log(fib(14)); 12 | 13 | case function fib 14 | when (0) 15 | 0 16 | when (1) 17 | 1 18 | when (n) 19 | fib(n - 1) + fib(n - 2) 20 | end 21 | 22 | console.log fib(14) 23 | -------------------------------------------------------------------------------- /format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # IK this sucks, but just to get things working for now 3 | $LOAD_PATH.push "/Users/marcelrusu/Documents/Projects/peacock/lib" 4 | 5 | require "lexer" 6 | require "parser" 7 | require "formatter" 8 | 9 | content = File.read(ARGV[0]) 10 | 11 | tokens = Lexer::tokenize(content) 12 | # # puts ast 13 | # # begin 14 | parser = Parser.new(tokens, content) 15 | File.write ARGV[0], Formatter.new(parser.parse!).eval 16 | # rescue Exception => e 17 | # puts e.stack 18 | # puts "---- FAILED ----" 19 | # end 20 | -------------------------------------------------------------------------------- /spec/codegen/classes/parser.pea: -------------------------------------------------------------------------------- 1 | # class Parser { 2 | # constructor(tokens, program_string, pos) { 3 | # this.tokens = tokens; 4 | # this.program_string = program_string; 5 | # this.pos = pos; 6 | # } 7 | # static from(that) { 8 | # return new this(that.tokens, that.program_string, that.pos); 9 | # } 10 | # }; 11 | 12 | class Parser 13 | function constructor(@tokens, @program_string, @pos) 14 | 15 | static function from(that) 16 | new this(that.tokens, that.program_string, that.pos) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /peacock-formatter/src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/todo_mvc/Form.pea: -------------------------------------------------------------------------------- 1 | component { items } 2 | new_item := "" 3 | in 4 |
emit! "update", new_item }}> 5 | 9 |
10 | 11 | 12 |
13 |
14 | with 15 | 24 | end 25 | -------------------------------------------------------------------------------- /spec/codegen/classes/token.pea: -------------------------------------------------------------------------------- 1 | # class Token { 2 | # constructor(raw_token, type, value = null, captures = null) { 3 | # this.raw_token = raw_token; 4 | # this.type = type; 5 | # this.value = value; 6 | # this.captures = captures; 7 | # } 8 | # }; 9 | # class Lexer { 10 | # constructor(str) { 11 | # this.str = str; 12 | # } 13 | # tokenize() { 14 | # 15 | # } 16 | # }; 17 | 18 | 19 | class Token 20 | function constructor(@raw_token, @type, @value = null, @captures = null) 21 | end 22 | 23 | class Lexer 24 | function constructor(@str) 25 | 26 | function tokenize 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /peacock-formatter/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /spec/codegen/case_function/factorial.pea: -------------------------------------------------------------------------------- 1 | # const Gt1 = (_it => _it > 1); 2 | # function factorial(...args) { 3 | # if (s.check([Gt1], args)) { 4 | # let n = args[0]; 5 | # return factorial(n - 1) + factorial(n - 2); 6 | # } else if (s.check([1], args)) { 7 | # return 1; 8 | # } else if (s.check([0], args)) { 9 | # return 0; 10 | # } else throw new MatchError(); 11 | # }; 12 | # console.log(factorial(20)); 13 | 14 | schema Gt1 = #{ % > 1 } 15 | 16 | case function factorial 17 | when (Gt1(n)) 18 | factorial(n - 1) + factorial(n - 2) 19 | when (1) 20 | 1 21 | when (0) 22 | 0 23 | end 24 | 25 | console.log factorial(20) 26 | -------------------------------------------------------------------------------- /spec/codegen/classes/static_from.pea: -------------------------------------------------------------------------------- 1 | # class Parser { 2 | # static from(that) { 3 | # return new this(that.tokens, that.program_string, that.pos); 4 | # } 5 | # constructor(tokens, program_string, pos) { 6 | # this.tokens = tokens; 7 | # this.program_string = program_string; 8 | # this.pos = pos; 9 | # } 10 | # }; 11 | 12 | class Parser 13 | static function from(that) 14 | new this(that.tokens, that.program_string, that.pos) 15 | end 16 | 17 | function constructor(tokens, program_string, pos) 18 | this.tokens := tokens 19 | this.program_string := program_string 20 | this.pos := pos 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /peacock-formatter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | ".vscode-test" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /spec/codegen/case_function/to_a.pea: -------------------------------------------------------------------------------- 1 | # function to_a(...args) { 2 | # if (s.check(Array, this) && s.check([], args)) { 3 | # return this; 4 | # } else if (s.check(String, this) && s.check([], args)) { 5 | # return Array.from(this); 6 | # } else if (s.check(Object, this) && s.check([], args)) { 7 | # return Object.entries(this); 8 | # } else throw new MatchError(); 9 | # }; 10 | # console.log(to_a.call({ a: 1 })); 11 | # console.log(to_a.call("abc")); 12 | 13 | case function to_a 14 | when Array::() 15 | this 16 | when String::() 17 | Array.from this 18 | when Object::() 19 | Object.entries this 20 | end 21 | 22 | console.log { a: 1 }::to_a 23 | console.log "abc"::to_a 24 | -------------------------------------------------------------------------------- /snapshot.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.push "./lib" 4 | 5 | require "lexer" 6 | require "parser" 7 | 8 | content = File.read(ARGV[0]) 9 | tokens = Lexer::tokenize(content) 10 | ast = Parser.new(tokens, content).parse! 11 | ast = ast.map(&:to_h) 12 | spec = <<-EOS 13 | it "#{Time::now}" do 14 | ast = parse('#{content}') 15 | expect(ast).to eq(#{ast}) 16 | end 17 | EOS 18 | 19 | SNAPSHOT_SPEC_FILENAME = "spec/snapshot_spec.rb" 20 | 21 | *file, end_line = File.read(SNAPSHOT_SPEC_FILENAME) 22 | .split("\n") 23 | 24 | throw "wtf" if end_line != "end" 25 | 26 | new_file = (file + spec.split("\n") + [end_line]).join("\n") 27 | 28 | File.write(SNAPSHOT_SPEC_FILENAME, new_file) 29 | -------------------------------------------------------------------------------- /examples/todo_mvc/Page.pea: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from "router" 2 | import List from "./List" 3 | import Form from "./Form" 4 | import TodoList from "./ActiveList" 5 | import TodoList from "./CompleteList" 6 | 7 | component 8 | items := [{ completed: false, desc: "finish app" }] 9 | in 10 |
11 |
items = items::append(new_item)} /> 12 | 13 | 18 |
19 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peacock", 3 | "version": "0.0.1", 4 | "description": "A joyful language aimed towards writing front end applications", 5 | "directories": { 6 | "lib": "lib" 7 | }, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/marcellerusu/peacock-lang.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/marcellerusu/peacock-lang/issues" 19 | }, 20 | "homepage": "https://github.com/marcellerusu/peacock-lang#readme", 21 | "dependencies": { 22 | "colors": "1.4.0", 23 | "express": "4.17.2", 24 | "jsdom": "^19.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/codegen/array/group_by.pea: -------------------------------------------------------------------------------- 1 | # let arr; 2 | # function group_by(key) { 3 | # if (!(this instanceof Array)) throw new MatchError('Expected `this` to be a `Array`'); 4 | # let result; 5 | # result = {}; 6 | # for (let obj of this) { 7 | # result[obj[key]] ||= []; 8 | # result[obj[key]].push(obj); 9 | # }; 10 | # return result; 11 | # }; 12 | # arr = [{ id: 1, value: 10 }, { id: 1, value: 10 }]; 13 | # console.log(group_by.call(arr, "id")); 14 | 15 | function Array::group_by(key) 16 | result := {} 17 | for obj of this 18 | result[obj[key]] ||= [] 19 | result[obj[key]].push(obj) 20 | end 21 | 22 | return result 23 | end 24 | 25 | arr := [{ id: 1, value: 10 }, { id: 1, value: 10 }] 26 | 27 | console.log arr::group_by "id" 28 | -------------------------------------------------------------------------------- /peacock-formatter/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /examples/todo_mvc/List.pea: -------------------------------------------------------------------------------- 1 | import { update } from "peacock" 2 | 3 | import { items } from "./items" 4 | 5 | component 6 | def toggleItemCompleted(id) 7 | $items := for $items of 8 | { id: id } as item => { ...item, completed: !item.completed }, 9 | else item => item 10 | end 11 | end 12 | newItem := "" 13 | in 14 |
15 | 16 |
    17 | #for { completed, desc, id } of $items 18 |
  • toggleItemCompleted id} 21 | > 22 | {desc} 23 |
  • 24 | end 25 |
26 |
27 | 28 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/pea_std_lib/array.pea: -------------------------------------------------------------------------------- 1 | function each(callback) 2 | for item of this 3 | callback item 4 | end 5 | end 6 | 7 | function Array::append(item) = [...this, item] 8 | 9 | case function zip 10 | when (first, second) 11 | first.map((x, i) => [x, second[i]]) 12 | when Array::(second) 13 | zip this, second 14 | end 15 | 16 | case function to_a 17 | when Array::() 18 | this 19 | when String::() 20 | Array.from this 21 | when Set::() 22 | Array.from this 23 | when Object::() 24 | Object.entries this 25 | end 26 | 27 | function Array::uniq() = 28 | this.filter((item1, i) => i == this.findIndex(item2 => item1 == item2)) 29 | 30 | function Array::group_by(key) 31 | result := {} 32 | for obj of this 33 | result[obj[key]] ||= [] 34 | result[obj[key]].push(obj) 35 | end 36 | 37 | return result 38 | end 39 | -------------------------------------------------------------------------------- /lib/pea_std_lib/range.js: -------------------------------------------------------------------------------- 1 | class Range { 2 | #filter_function; 3 | constructor(from, to, filter = () => true) { 4 | this.from = from; 5 | this.to = to; 6 | this.#filter_function = filter; 7 | } 8 | contains(num) { 9 | return num <= this.to && num >= this.from; 10 | } 11 | map(fn) { 12 | return new Range(fn(this.from), fn(this.to)); 13 | } 14 | filter(fn) { 15 | return new Range(this.from, this.to, fn); 16 | } 17 | *[Symbol.iterator]() { 18 | let val = this.from; 19 | while (val <= this.to) { 20 | if (this.#filter_function(val)) { 21 | yield val; 22 | } 23 | val = val[Symbol.peacock_next](); 24 | } 25 | } 26 | [Symbol.peacock_equals](other) { 27 | if (!(other instanceof Range)) return false; 28 | return this.from === other.from && this.to === other.to; 29 | } 30 | [Symbol.peacock_contains](value) { 31 | return this.contains(value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /peacock-formatter/src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /examples/css_parser/lexer.pea: -------------------------------------------------------------------------------- 1 | class Token 2 | function constructor(@raw_token, @start_pos, @end_pos, @type = raw_token, @value = null, @captures = null) 3 | 4 | function is_a(type) = this.type == type 5 | function is_not_a(type) = !this.is_a(type) 6 | function is_one_of(...types) = types.includes(this.type) 7 | function is_not_one_of(...types) = !this.is_one_of(...types) 8 | end 9 | 10 | class Lexer 11 | function constructor(@str) 12 | 13 | index := 0 14 | get rest_of_str = this.str.slice(this.index) 15 | get is_end_of_str = this.index >= this.str.length 16 | 17 | 18 | function scan(regex) 19 | result := this.rest_of_str.match(new Regex(regex)) 20 | if result === null || result.index !== 0 21 | return false 22 | end 23 | this.matched := result[0] 24 | this.index += this.matched.length 25 | return true 26 | end 27 | 28 | function tokenize 29 | case 30 | when this.is_end_of_str 31 | 10 32 | when this.scan("+") 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /main.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push "./lib" 2 | 3 | require "lexer" 4 | require "parser" 5 | require "type_checker" 6 | require "compiler" 7 | 8 | content = File.read(ARGV[0]) 9 | 10 | tokens = Lexer::tokenize(content) 11 | # # puts ast 12 | # # begin 13 | parser = Parser.new(tokens, content) 14 | 15 | case ARGV[1] 16 | when "-t" 17 | puts "[" 18 | puts " #{tokens.map(&:to_s).join ",\n "}" 19 | puts "]" 20 | when /-a+/ 21 | ast = parser.parse! 22 | pp ast.map(&:to_h) 23 | when "-n" 24 | ast = parser.parse! 25 | when "-c" 26 | ast = parser.parse! 27 | ast = TypeChecker.new(ast, content).step! 28 | js = Compiler.new(ast, bundle_std_lib: true).eval 29 | puts js 30 | when "-h" 31 | ast = parser.parse! 32 | js = Compiler.new(ast, bundle_std_lib: true).eval 33 | puts " 34 | 35 | 36 | 37 | 40 | 41 | " 42 | else 43 | ast = parser.parse! 44 | js = Compiler.new(ast, bundle_std_lib: true).eval 45 | puts js 46 | end 47 | # rescue Exception => e 48 | # puts e.stack 49 | # puts "---- FAILED ----" 50 | # end 51 | -------------------------------------------------------------------------------- /spec/snapshot_spec.rb: -------------------------------------------------------------------------------- 1 | require "ast" 2 | require "parser" 3 | require "compiler" 4 | 5 | def compile(str) 6 | tokens = Lexer::tokenize(str.strip) 7 | ast = Parser.new(tokens, str).parse! 8 | Compiler.new(ast).eval 9 | end 10 | 11 | def entries(dir) 12 | Dir.entries(dir).select { |file| ![".", ".."].include? file } 13 | end 14 | 15 | entries("spec/codegen").each do |category| 16 | context category do 17 | entries("spec/codegen/#{category}").each do |name| 18 | contents = File.read("spec/codegen/#{category}/#{name}") 19 | f = false 20 | i = false 21 | case contents[0..1] 22 | when ":f" 23 | f = true 24 | contents.slice! ":f" 25 | when ":i" 26 | i = true 27 | contents.slice! ":i" 28 | end 29 | begin 30 | it name, f: f, i: i do 31 | output = compile(contents) 32 | expected_output = contents.split("\n") 33 | .select { |line| line.start_with? "#" } 34 | .map { |line| line[2..].rstrip }.join "\n" 35 | 36 | expect(output).to eq(expected_output) 37 | end 38 | rescue 39 | binding.pry 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /peacock-formatter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peacock-formatter", 3 | "displayName": "peacock-formatter", 4 | "description": "", 5 | "version": "0.0.1", 6 | "engines": { 7 | "vscode": "^1.64.0" 8 | }, 9 | "categories": [ 10 | "Other" 11 | ], 12 | "activationEvents": [ 13 | "onLanguage:peacock" 14 | ], 15 | "main": "./dist/extension.js", 16 | "contributes": { 17 | "commands": [ 18 | { 19 | "command": "peacock-formatter.helloWorld", 20 | "title": "Hello World" 21 | } 22 | ] 23 | }, 24 | "scripts": { 25 | "vscode:prepublish": "yarn run package", 26 | "compile": "webpack", 27 | "watch": "webpack --watch", 28 | "package": "webpack --mode production --devtool hidden-source-map", 29 | "compile-tests": "tsc -p . --outDir out", 30 | "watch-tests": "tsc -p . -w --outDir out", 31 | "pretest": "yarn run compile-tests && yarn run compile && yarn run lint", 32 | "lint": "eslint src --ext ts", 33 | "test": "node ./out/test/runTest.js" 34 | }, 35 | "devDependencies": { 36 | "@types/vscode": "^1.64.0", 37 | "@types/glob": "^7.1.4", 38 | "@types/mocha": "^9.0.0", 39 | "@types/node": "14.x", 40 | "@typescript-eslint/eslint-plugin": "^5.1.0", 41 | "@typescript-eslint/parser": "^5.1.0", 42 | "eslint": "^8.1.0", 43 | "glob": "^7.1.7", 44 | "mocha": "^9.1.3", 45 | "typescript": "^4.4.4", 46 | "ts-loader": "^9.2.5", 47 | "webpack": "^5.52.1", 48 | "webpack-cli": "^4.8.0", 49 | "@vscode/test-electron": "^1.6.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /peacock-formatter/webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'nosources-source-map', 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [ extensionConfig ]; -------------------------------------------------------------------------------- /examples/parser/parser.pea: -------------------------------------------------------------------------------- 1 | import { Tokens } from "./lexer" 2 | 3 | class Parser 4 | static function from(that) 5 | new this(that.tokens, that.program_string, that.pos) 6 | end 7 | 8 | case function constructor 9 | when (tokens, program_string, pos) 10 | this.tokens := tokens 11 | this.program_string := program_string 12 | this.pos := pos 13 | when (tokens, program_string) 14 | new this(tokens, program_string, 0) 15 | end 16 | 17 | get current_token = this.tokens[this.pos] 18 | get prev_token = this.tokens[this.pos - 1] 19 | get peek_token = this.tokens[this.pos + 1] 20 | 21 | function consume_parser!(parser_klass) 22 | parser := parser_klass.from(this) 23 | expr_n := parser.parse! 24 | this.pos := parser.pos 25 | return expr_n 26 | end 27 | 28 | case function consume! 29 | when (token_type) 30 | assert token_type === this.current_token.type 31 | return this.consume! 32 | else 33 | this.pos += 1 34 | return this.prev_token 35 | end 36 | 37 | function parse! 38 | ProgramParser.from(this).parse! 39 | end 40 | end 41 | 42 | class ProgramParser < Parser 43 | body = [] 44 | 45 | ALLOWED_PARSERS = [ 46 | SimpleAssignmentParser 47 | ] 48 | 49 | function consume_parser!(parser_klass) 50 | this.body.push super(parser_klass) 51 | end 52 | 53 | function parse! 54 | while this.current_token?.is_not_one_of?("end", "}") 55 | consume_parser! this.ALLOWED_PARSERS.find #{ it.can_parse? this } 56 | end 57 | 58 | return this.body 59 | end 60 | end 61 | 62 | class SimpleAssignmentParser < Parser 63 | static function can_parse?(that) 64 | that.current_token?.type == "identifier" && 65 | that.peek_type?.type == "assign" 66 | end 67 | 68 | function parse! 69 | id_t := consume! "identifier" 70 | consume! "assign" 71 | expr_n := consume_parser! ExprParser 72 | new AST.SimpleAssignment( 73 | id_t.value, 74 | expr_n, 75 | id_t.start_pos, 76 | id_t.end_pos 77 | ) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | backport (1.2.0) 6 | benchmark (0.2.0) 7 | diff-lcs (1.4.4) 8 | e2mmap (0.1.0) 9 | jaro_winkler (1.5.4) 10 | kramdown (2.3.1) 11 | rexml 12 | kramdown-parser-gfm (1.1.0) 13 | kramdown (~> 2.0) 14 | nokogiri (1.12.5-x86_64-darwin) 15 | racc (~> 1.4) 16 | opal (1.3.2) 17 | ast (>= 2.3.0) 18 | parser (~> 3.0) 19 | parallel (1.21.0) 20 | parser (3.0.3.2) 21 | ast (~> 2.4.1) 22 | racc (1.6.0) 23 | rainbow (3.0.0) 24 | regexp_parser (2.2.0) 25 | reverse_markdown (2.1.1) 26 | nokogiri 27 | rexml (3.2.5) 28 | rspec (3.10.0) 29 | rspec-core (~> 3.10.0) 30 | rspec-expectations (~> 3.10.0) 31 | rspec-mocks (~> 3.10.0) 32 | rspec-core (3.10.1) 33 | rspec-support (~> 3.10.0) 34 | rspec-expectations (3.10.1) 35 | diff-lcs (>= 1.2.0, < 2.0) 36 | rspec-support (~> 3.10.0) 37 | rspec-mocks (3.10.2) 38 | diff-lcs (>= 1.2.0, < 2.0) 39 | rspec-support (~> 3.10.0) 40 | rspec-support (3.10.3) 41 | rubocop (1.24.0) 42 | parallel (~> 1.10) 43 | parser (>= 3.0.0.0) 44 | rainbow (>= 2.2.2, < 4.0) 45 | regexp_parser (>= 1.8, < 3.0) 46 | rexml 47 | rubocop-ast (>= 1.15.0, < 2.0) 48 | ruby-progressbar (~> 1.7) 49 | unicode-display_width (>= 1.4.0, < 3.0) 50 | rubocop-ast (1.15.1) 51 | parser (>= 3.0.1.1) 52 | ruby-progressbar (1.11.0) 53 | solargraph (0.44.2) 54 | backport (~> 1.2) 55 | benchmark 56 | bundler (>= 1.17.2) 57 | diff-lcs (~> 1.4) 58 | e2mmap 59 | jaro_winkler (~> 1.5) 60 | kramdown (~> 2.3) 61 | kramdown-parser-gfm (~> 1.1) 62 | parser (~> 3.0) 63 | reverse_markdown (>= 1.0.5, < 3) 64 | rubocop (>= 0.52) 65 | thor (~> 1.0) 66 | tilt (~> 2.0) 67 | yard (~> 0.9, >= 0.9.24) 68 | thor (1.1.0) 69 | tilt (2.0.10) 70 | unicode-display_width (2.1.0) 71 | webrick (1.7.0) 72 | yard (0.9.27) 73 | webrick (~> 1.7.0) 74 | 75 | PLATFORMS 76 | universal-darwin-21 77 | x86_64-darwin-20 78 | 79 | DEPENDENCIES 80 | opal 81 | rspec (~> 3.10) 82 | solargraph 83 | 84 | BUNDLED WITH 85 | 2.2.22 86 | -------------------------------------------------------------------------------- /peacock-formatter/src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from "vscode"; 4 | import { exec } from "child_process"; 5 | 6 | // this method is called when your extension is activated 7 | // your extension is activated the very first time the command is executed 8 | export function activate(context: vscode.ExtensionContext) { 9 | // Use the console to output diagnostic information (console.log) and errors (console.error) 10 | // This line of code will only be executed once when your extension is activated 11 | console.log( 12 | 'Congratulations, your extension "peacock-formatter" is now active!' 13 | ); 14 | 15 | // The command has been defined in the package.json file 16 | // Now provide the implementation of the command with registerCommand 17 | // The commandId parameter must match the command field in package.json 18 | let disposable = vscode.commands.registerCommand( 19 | "peacock-formatter.helloWorld", 20 | (e) => { 21 | // vscode.debug.activeDebugConsole.append("wdtf"); 22 | // console.log(e); 23 | // if (e.fileName.indexOf(".pea") + 4 !== e.fileName.length) return; 24 | // The code you place here will be executed every time your command is executed 25 | // Display a message box to the user 26 | 27 | var onSave = vscode.workspace.onDidSaveTextDocument( 28 | (e: vscode.TextDocument) => { 29 | // execute some child process on save 30 | const editor = vscode.window.activeTextEditor; 31 | if (!editor) return; 32 | const position = editor?.selection; 33 | var child = exec("format " + e.fileName); 34 | child.stdout?.on("data", (data) => { 35 | vscode.window.showInformationMessage(data); 36 | editor.selection = position; 37 | }); 38 | child.stderr?.on("data", (data) => { 39 | vscode.window.showErrorMessage(data); 40 | }); 41 | } 42 | ); 43 | context.subscriptions.push(onSave); 44 | vscode.window.showInformationMessage( 45 | "Hello World from peacock-formatter!" 46 | ); 47 | } 48 | ); 49 | 50 | context.subscriptions.push(disposable); 51 | } 52 | 53 | // this method is called when your extension is deactivated 54 | export function deactivate() {} 55 | -------------------------------------------------------------------------------- /peacock-formatter/README.md: -------------------------------------------------------------------------------- 1 | # peacock-formatter README 2 | 3 | This is the README for your extension "peacock-formatter". After writing up a brief description, we recommend including the following sections. 4 | 5 | ## Features 6 | 7 | Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. 8 | 9 | For example if there is an image subfolder under your extension project workspace: 10 | 11 | \!\[feature X\]\(images/feature-x.png\) 12 | 13 | > Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. 14 | 15 | ## Requirements 16 | 17 | If you have any requirements or dependencies, add a section describing those and how to install and configure them. 18 | 19 | ## Extension Settings 20 | 21 | Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. 22 | 23 | For example: 24 | 25 | This extension contributes the following settings: 26 | 27 | * `myExtension.enable`: enable/disable this extension 28 | * `myExtension.thing`: set to `blah` to do something 29 | 30 | ## Known Issues 31 | 32 | Calling out known issues can help limit users opening duplicate issues against your extension. 33 | 34 | ## Release Notes 35 | 36 | Users appreciate release notes as you update your extension. 37 | 38 | ### 1.0.0 39 | 40 | Initial release of ... 41 | 42 | ### 1.0.1 43 | 44 | Fixed issue #. 45 | 46 | ### 1.1.0 47 | 48 | Added features X, Y, and Z. 49 | 50 | ----------------------------------------------------------------------------------------------------------- 51 | ## Following extension guidelines 52 | 53 | Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. 54 | 55 | * [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) 56 | 57 | ## Working with Markdown 58 | 59 | **Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: 60 | 61 | * Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux) 62 | * Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux) 63 | * Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets 64 | 65 | ### For more information 66 | 67 | * [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) 68 | * [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) 69 | 70 | **Enjoy!** 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Not Active Project 2 | 3 | Peacock was a language I designed when I observed a lot of code like this. 4 | 5 | ```jsx 6 | function PersonalYouTubeChannelLibrary() { 7 | let [videos, setVideos] = useState(null); 8 | let [error, setError] = useState(null); 9 | useEffect(() => { 10 | fetch(API.videos) 11 | .then((r) => r.json()) 12 | .then(setVideos) 13 | .catch(setError); 14 | }, []); 15 | if (error) { 16 | return
You got an error :(
; 17 | } else if (!videos) { 18 | return
You Have no Videos yet! Make one
; 19 | } else if (videos.length === 1) { 20 | if (firstTimeViewingChannelSinceCreated(videos[0])) 21 | return
Congrats on creating your first video
; 22 | } else if (videos.length >= 50) { 23 | return
Congrats on 50 videos, considering upgrading to X tier
; 24 | } else { 25 | return
here are your videos
; 26 | } 27 | } 28 | ``` 29 | 30 | Expressing complex states in vanilla js is hard. 31 | 32 | At the same time, I did some small projects in Elixir and I was blown away by the expressive power of pattern matching. 33 | 34 | I also read up a fair bit on clojure.spec, and the ideas of "patterns" as data. I thought, that's where peacock came out of: 35 | 36 | The power of clojure's pattern as data, inspired by the usability from elixir and the syntax from typescript. 37 | 38 | Patterns are expressive, but if they are not data, they cannot be reused. 39 | 40 | Peacock patterns are simple data structures, and you can store patterns in variables and create new patterns from combining old patterns. 41 | 42 | Here's some peacock code 43 | 44 | ``` 45 | schema User = {name, age} 46 | 47 | User(me) := {name: "marcelle", age: 25} 48 | ``` 49 | 50 | Schemas are pattern definitions, and what we were doing here was creating a variable 'me' who was validated by 'User'. 51 | 52 | You could also do this 53 | 54 | ``` 55 | schema Teenager = User & {age: #{ it < 20 }} 56 | 57 | Teenager(me) := me # throws error 58 | ``` 59 | 60 | We've combined the User schema with a new pattern, and peacock holds me accountable in pretending to be a hip teen. 61 | 62 | The goal of peacock was to take patterns and make them approachable to js/ts folks. 63 | 64 | The aspirations for peacock was to be a full blown frontend framework, but this I think was the ultimately the downfall, it tried to do too much. 65 | 66 | Another flaw I found with this system is that pattern matching is fundamentally limited, it developed closed systems that can not be extended from the outside. 67 | 68 | There are many times this is great - state machines for example, but as a basis for a language it is far too limited. 69 | 70 | To see more recent work, check out my other language coil-lang. 71 | -------------------------------------------------------------------------------- /peacock-formatter/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 37 | * Press `F5` to run the tests in a new window with your extension loaded. 38 | * See the output of the test result in the debug console. 39 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 40 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 41 | * You can create folders inside the `test` folder to structure your tests any way you want. 42 | 43 | ## Go further 44 | 45 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 46 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 47 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 48 | -------------------------------------------------------------------------------- /lib/formatter.rb: -------------------------------------------------------------------------------- 1 | require "ast" 2 | 3 | class Formatter 4 | attr_reader :context 5 | 6 | def initialize(ast, context = Context.new, indentation = 0) 7 | @ast = ast 8 | @context = context 9 | @indentation = indentation 10 | end 11 | 12 | # maybe take these methods out into an `Indenter` class 13 | def indent! 14 | @indentation += 2 15 | end 16 | 17 | def peek_indent 18 | @indentation + 2 19 | end 20 | 21 | def indentation 22 | " " * @indentation 23 | end 24 | 25 | def dedent! 26 | assert { @indentation >= 2 } 27 | @indentation -= 2 28 | end 29 | 30 | def last_node? 31 | @is_last_node ||= false 32 | end 33 | 34 | def last_node! 35 | @is_last_node = true 36 | end 37 | 38 | def eval 39 | output = "" 40 | index = 0 41 | for node in @ast 42 | last_node! if index == @ast.size - 1 43 | result = indentation + eval_node(node) + "\n" 44 | output += result unless result.strip.empty? 45 | index += 1 46 | end 47 | output.strip! if context.directly_in_a?(:short_fn) 48 | output 49 | end 50 | 51 | def eval_node(node) 52 | case node 53 | when AST::Assign 54 | eval_assign node 55 | when AST::Int 56 | eval_int node 57 | when AST::List 58 | eval_list node 59 | when AST::ObjectLiteral 60 | eval_object_literal node 61 | when AST::OpCall 62 | eval_op_call node 63 | when AST::ShortFn 64 | eval_short_fn node 65 | when AST::Fn 66 | eval_anon_fn node 67 | when AST::Return 68 | eval_return node 69 | when AST::IdLookup 70 | eval_id_lookup node 71 | else 72 | pp node.class 73 | assert_not_reached 74 | end 75 | end 76 | 77 | def eval_assign(node) 78 | return "" if node.name == "pea_module" 79 | "#{node.name} := #{eval_node node.expr}" 80 | end 81 | 82 | def eval_int(node) 83 | node.value 84 | end 85 | 86 | def eval_list(node) 87 | "[#{node.value.map { |n| eval_node n }.join ", "}]" 88 | end 89 | 90 | def eval_record(node) 91 | r = "{" 92 | context.push! :record 93 | node.value.map do |key, value| 94 | r += " #{eval_node key}: #{eval_node value} " 95 | end 96 | context.pop! :record 97 | r + "}" 98 | end 99 | 100 | def eval_id_lookup(node) 101 | return "%" if node.value == ANON_SHORTHAND_ID 102 | node.value 103 | end 104 | 105 | def eval_return(node) 106 | return_val = eval_node node.value 107 | if context.directly_in_a?(:short_fn) || (last_node? && context.directly_in_a?(:anon_fn)) 108 | return_val 109 | else 110 | "return #{return_val}" 111 | end 112 | end 113 | 114 | def eval_short_fn(node) 115 | '#{ ' + Formatter.new(node.body, context.push(:short_fn)).eval + " }" 116 | end 117 | 118 | def eval_anon_fn(node) 119 | fn = "do |#{node.args.join(", ").strip}|\n" 120 | fn += Formatter.new(node.body, context.push(:anon_fn), peek_indent).eval 121 | fn += "end" 122 | end 123 | 124 | def eval_op_call(node) 125 | rhs = node.args.first 126 | lhs = node.expr.lhs_expr 127 | method = node.expr.property 128 | 129 | "#{eval_node lhs} #{METHOD_TO_OP[method]} #{eval_node rhs}" 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/pea_std_lib/symbols.js: -------------------------------------------------------------------------------- 1 | Symbol.peacock_contains = Symbol("peacock_contains"); 2 | Symbol.peacock_equals = Symbol("peacock_equals"); 3 | Symbol.peacock_next = Symbol("peacock_next"); 4 | 5 | Number.prototype[Symbol.peacock_next] = function () { 6 | return this + 1; 7 | }; 8 | 9 | String.prototype[Symbol.peacock_next] = function () { 10 | return String.fromCharCode(this.charCodeAt(0) + 1); 11 | }; 12 | 13 | BigInt.prototype[Symbol.peacock_next] = function () { 14 | return this + 1; 15 | }; 16 | 17 | Number.prototype[Symbol.peacock_equals] = function (other) { 18 | return this === other; 19 | }; 20 | 21 | String.prototype[Symbol.peacock_equals] = function (other) { 22 | return this === other; 23 | }; 24 | 25 | BigInt.prototype[Symbol.peacock_equals] = function (other) { 26 | return this === other; 27 | }; 28 | 29 | Boolean.prototype[Symbol.peacock_equals] = function (other) { 30 | return this === other; 31 | }; 32 | 33 | Date.prototype[Symbol.peacock_equals] = function (other) { 34 | if (!(other instanceof Date)) return false; 35 | return this.valueOf() === other.valueOf(); 36 | }; 37 | 38 | RegExp.prototype[Symbol.peacock_equals] = function (other) { 39 | return this.source === other.source; 40 | }; 41 | 42 | Set.prototype[Symbol.peacock_contains] = function (val) { 43 | if (this.has(val)) return true; 44 | return Array.from(this).some((v) => v[Symbol.peacock_equals](val)); 45 | }; 46 | 47 | Set.prototype[Symbol.peacock_equals] = function (other) { 48 | if (!(other instanceof Set)) return false; 49 | if (other.size !== this.size) return false; 50 | let entries; 51 | 52 | for (let val of this) { 53 | if (other.has(val)) continue; 54 | entries ||= Array.from(other); 55 | if (!entries.some((v) => v === val || v[Symbol.peacock_equals](val))) 56 | return false; 57 | } 58 | return true; 59 | }; 60 | 61 | Array.prototype[Symbol.peacock_equals] = function (other) { 62 | if (!(other instanceof Array)) return false; 63 | if (this.length !== other.length) return false; 64 | for (let i = 0; i < this.length; i++) { 65 | if (this[i] === other[i]) continue; 66 | if (!this[i][Symbol.peacock_equals](other[i])) return false; 67 | } 68 | return true; 69 | }; 70 | 71 | Array.prototype[Symbol.peacock_contains] = function (value) { 72 | if (this.includes(value)) return true; 73 | for (let _value of this) { 74 | if (_value === value) return true; 75 | if (_value[Symbol.peacock_equals](value)) return true; 76 | } 77 | return false; 78 | }; 79 | 80 | Map.prototype[Symbol.peacock_equals] = function (other) { 81 | if (!(other instanceof Map)) return false; 82 | if (this.size !== other.size) return false; 83 | for (let [key, value] of this) { 84 | if (!other.has(key)) return false; 85 | let _value = other.get(key); 86 | if (_value === value) continue; 87 | if (!_value[Symbol.peacock_equals](value)) return false; 88 | } 89 | return true; 90 | }; 91 | 92 | Map.prototype[Symbol.peacock_contains] = function ([key, value]) { 93 | if (!this.has(key)) return false; 94 | let _value = this.get(key); 95 | return _value === value || _value[Symbol.peacock_equals](value); 96 | }; 97 | 98 | Object.prototype[Symbol.peacock_equals] = function (other) { 99 | if (this.constructor !== Object || other.constructor !== Object) 100 | throw "Not implemented"; 101 | if (Object.keys(this).length !== Object.keys(other).length) return false; 102 | for (let [key, value] of Object.entries(this)) { 103 | if (!other[key]) return false; 104 | if (other[key] === value) continue; 105 | if (!other[key][Symbol.peacock_equals](value)) return false; 106 | } 107 | return true; 108 | }; 109 | -------------------------------------------------------------------------------- /lib/pea_std_lib/schema.js: -------------------------------------------------------------------------------- 1 | class ArrayLengthMismatch extends Error {} 2 | class MatchError extends Error {} 3 | 4 | const BUILTIN_CONSTRUCTORS = [ 5 | Boolean, 6 | String, 7 | RegExp, 8 | Symbol, 9 | Error, 10 | AggregateError, 11 | EvalError, 12 | RangeError, 13 | ReferenceError, 14 | URIError, 15 | SyntaxError, 16 | TypeError, 17 | Number, 18 | BigInt, 19 | Date, 20 | Function, 21 | Array, 22 | Map, 23 | Set, 24 | WeakMap, 25 | WeakSet, 26 | Promise, 27 | ]; 28 | 29 | function isPrimitivePattern(pattern) { 30 | return BUILTIN_CONSTRUCTORS.includes(pattern); 31 | } 32 | 33 | function isCustomClass(pattern) { 34 | return ( 35 | pattern?.prototype?.constructor?.toString()?.substring(0, 5) === "class" 36 | ); 37 | } 38 | 39 | class Any { 40 | [Symbol.peacock_equals](other) { 41 | return other instanceof Any; 42 | } 43 | } 44 | 45 | class Capture { 46 | constructor(key) { 47 | this.key = key; 48 | } 49 | [Symbol.peacock_equals](other) { 50 | if (!(other instanceof Capture)) return false; 51 | return this.key === other.key; 52 | } 53 | } 54 | 55 | class Union { 56 | constructor(patterns) { 57 | this.patterns = patterns; 58 | } 59 | [Symbol.peacock_equals](other) { 60 | if (!(other instanceof Union)) return false; 61 | return this.patterns[Symbol.peacock_equals](other.patterns); 62 | } 63 | [Symbol.peacock_contains](value) { 64 | return this.patterns[Symbol.peacock_contains](value); 65 | } 66 | [Symbol.iterator]() { 67 | return this.patterns[Symbol.iterator](); 68 | } 69 | } 70 | 71 | function s(key) { 72 | return new Capture(key); 73 | } 74 | 75 | s.checkCapture = function checkCapture(captures, key, value) { 76 | if (captures.has(key)) { 77 | return captures.get(key) === value; 78 | } 79 | captures.set(key, value); 80 | return true; 81 | }; 82 | 83 | s.checkObject = function checkObject(pattern, value, captures) { 84 | if (value.constructor !== Object) return false; 85 | for (let key in pattern) { 86 | if (typeof value[key] === "undefined") return false; 87 | if (!s.check(pattern[key], value[key], captures)) return false; 88 | } 89 | return true; 90 | }; 91 | 92 | s.checkRegExp = function checkRegExp(pattern, value) { 93 | return pattern.test(value); 94 | }; 95 | 96 | s.checkUnion = function checkUnion({ patterns }, value) { 97 | return patterns.some((p) => s.check(p, value)); 98 | }; 99 | 100 | s.checkFn = function checkFn(patternFn, value) { 101 | return patternFn(value); 102 | }; 103 | 104 | const PRIMITIVES = [ 105 | "string", 106 | "undefined", 107 | "boolean", 108 | "number", 109 | "bigint", 110 | "string", 111 | "symbol", 112 | ]; 113 | 114 | s.checkInstance = function checkInstance(PatternClass, value) { 115 | if (PRIMITIVES.includes(typeof value)) return false; 116 | if (value) return value.constructor === PatternClass; 117 | }; 118 | 119 | function _zip(otherArray) { 120 | return this.map((x, i) => [x, otherArray[i]]); 121 | } 122 | 123 | s.checkArray = function checkArray(patternArr, valueArr, captures = new Map()) { 124 | if (patternArr.length !== valueArr.length) return false; 125 | for (let [pattern, value] of _zip.call(patternArr, valueArr)) { 126 | if (!s.check(pattern, value, captures)) return false; 127 | } 128 | return true; 129 | }; 130 | 131 | s.any = new Any(); 132 | 133 | s.check = function check(pattern, value, captures = new Map()) { 134 | if (pattern instanceof Any) { 135 | return true; 136 | } else if (pattern === null) { 137 | return value === null; 138 | } else if (pattern instanceof Capture) { 139 | return s.checkCapture(captures, pattern.key, value); 140 | } else if (pattern instanceof Union) { 141 | return s.checkUnion(pattern, value); 142 | } else if (pattern instanceof RegExp) { 143 | return s.checkRegExp(pattern, value); 144 | } else if (pattern instanceof Array) { 145 | return s.checkArray(pattern, value, captures); 146 | } else if (pattern.constructor === Object) { 147 | return s.checkObject(pattern, value, captures); 148 | } else if (isPrimitivePattern(pattern) || isCustomClass(pattern)) { 149 | return s.checkInstance(pattern, value); 150 | } else if (typeof pattern === "function") { 151 | return s.checkFn(pattern, value); 152 | } else { 153 | return pattern === value; 154 | } 155 | }; 156 | 157 | s.verify = function verify(pattern, value, pattern_name = "RIP") { 158 | if (s.check(pattern, value)) { 159 | return value; 160 | } else { 161 | throw new MatchError( 162 | ` 163 | Match Error! 164 | > '${value}' could not conform to '${pattern_name}' 165 | ` 166 | ); 167 | } 168 | }; 169 | 170 | s.union = (...patterns) => new Union(patterns); 171 | -------------------------------------------------------------------------------- /spec/lexer_spec.rb: -------------------------------------------------------------------------------- 1 | require "lexer" 2 | 3 | describe Lexer, "#tokenize" do 4 | describe "single tokens" do 5 | it "should tokenize variable" do 6 | res = Lexer::tokenize("variable_name!") 7 | expect(res).to eq([ 8 | Lexer::Token.new("variable_name!", :identifier, "variable_name!"), 9 | ]) 10 | end 11 | 12 | it "true" do 13 | res = Lexer::tokenize("true") 14 | expect(res).to eq([ 15 | Lexer::Token.new("true", :bool_lit, true), 16 | ]) 17 | end 18 | 19 | it "false" do 20 | res = Lexer::tokenize("false") 21 | expect(res).to eq([Lexer::Token.new("false", :bool_lit, false)]) 22 | end 23 | 24 | it "should ignore comments" do 25 | res = Lexer::tokenize("# something-ok = ") 26 | expect(res).to eq([]) 27 | end 28 | 29 | it "identifier 'a'" do 30 | res = Lexer::tokenize("a") 31 | expect(res).to eq([ 32 | Lexer::Token.new("a", :identifier, "a"), 33 | ]) 34 | end 35 | 36 | it "=" do 37 | res = Lexer::tokenize("=") 38 | expect(res).to eq([ 39 | Lexer::Token.new("=", :"="), 40 | ]) 41 | end 42 | 43 | it ":=" do 44 | res = Lexer::tokenize(":=") 45 | expect(res).to eq([ 46 | Lexer::Token.new(":=", :assign), 47 | ]) 48 | end 49 | 50 | it "30" do 51 | res = Lexer::tokenize("30") 52 | expect(res).to eq([ 53 | Lexer::Token.new("30", :int_lit, 30), 54 | ]) 55 | end 56 | 57 | it "30.5" do 58 | res = Lexer::tokenize("30.5") 59 | expect(res).to eq([ 60 | Lexer::Token.new("30.5", :float_lit, 30.5), 61 | ]) 62 | end 63 | 64 | it "\"string 3.0\"" do 65 | res = Lexer::tokenize("\"string 3.0\"") 66 | expect(res).to eq([ 67 | Lexer::Token.new('"string 3.0"', :str_lit, "string 3.0", []), 68 | ]) 69 | end 70 | 71 | it "string with space" do 72 | res = Lexer::tokenize(" \"some string\"") 73 | 74 | expect(res).to eq([ 75 | Lexer::Token.new(' "some string"', :str_lit, "some string", []), 76 | ]) 77 | end 78 | 79 | it "string with no space at start" do 80 | res = Lexer::tokenize("\"some string\"") 81 | expect(res).to eq([ 82 | Lexer::Token.new('"some string', :str_lit, "some string", []), 83 | ]) 84 | end 85 | 86 | it ":symbol", :i do 87 | res = Lexer::tokenize(":symbol") 88 | expect(res).to eq([ 89 | Lexer::Token.new(":symbol", :symbol, "symbol"), 90 | ]) 91 | end 92 | end 93 | 94 | describe "single-line" do 95 | it "a := 3" do 96 | res = Lexer::tokenize("a := 3") 97 | expect(res).to eq([ 98 | Lexer::Token.new("a", :identifier, "a"), 99 | Lexer::Token.new(":=", :assign), 100 | Lexer::Token.new("3", :int_lit, 3), 101 | ]) 102 | end 103 | it "a := 3 === 4.0" do 104 | res = Lexer::tokenize("a := 3 === 4.0") 105 | expect(res).to eq([ 106 | Lexer::Token.new("a", :identifier, "a"), 107 | Lexer::Token.new(":=", :assign), 108 | Lexer::Token.new("3", :int_lit, 3), 109 | Lexer::Token.new("===", :"==="), 110 | Lexer::Token.new("4.0", :float_lit, 4.0), 111 | ]) 112 | end 113 | it "a := 3 === \"4\"" do 114 | res = Lexer::tokenize("a := 3 === \"4\"") 115 | expect(res).to eq([ 116 | Lexer::Token.new("a", :identifier, "a"), 117 | Lexer::Token.new(":=", :assign), 118 | Lexer::Token.new("3", :int_lit, 3), 119 | Lexer::Token.new("===", :"==="), 120 | Lexer::Token.new('"4"', :str_lit, "4", []), 121 | ]) 122 | end 123 | describe "array" do 124 | it "[1]" do 125 | res = Lexer::tokenize("[1]") 126 | expect(res).to eq([ 127 | Lexer::Token.new("[", :"["), 128 | Lexer::Token.new("1", :int_lit, 1), 129 | Lexer::Token.new("]", :"]"), 130 | ]) 131 | end 132 | it "[ 1 ]" do 133 | res = Lexer::tokenize("[ 1 ]") 134 | expect(res).to eq([ 135 | Lexer::Token.new("[", :"["), 136 | Lexer::Token.new("1", :int_lit, 1), 137 | Lexer::Token.new("]", :"]"), 138 | ]) 139 | end 140 | end 141 | describe "record" do 142 | it "{a: 3}" do 143 | res = Lexer::tokenize("{a: 3}") 144 | expect(res).to eq([ 145 | Lexer::Token.new("{", :"{"), 146 | Lexer::Token.new("a", :identifier, "a"), 147 | Lexer::Token.new(":", :colon), 148 | Lexer::Token.new("3", :int_lit, 3), 149 | Lexer::Token.new("}", :"}"), 150 | ]) 151 | end 152 | it "{ a : 3, }" do 153 | res = Lexer::tokenize("{ a : 3, }") 154 | expect(res).to eq([ 155 | Lexer::Token.new("{", :"{"), 156 | Lexer::Token.new("a", :identifier, "a"), 157 | Lexer::Token.new(":", :colon), 158 | Lexer::Token.new("3", :int_lit, 3), 159 | Lexer::Token.new(",", :comma), 160 | Lexer::Token.new("}", :"}"), 161 | ]) 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | config.filter_run_when_matching :f 18 | config.filter_run_excluding :i 19 | # rspec-expectations config goes here. You can use an alternate 20 | # assertion/expectation library such as wrong or the stdlib/minitest 21 | # assertions if you prefer. 22 | config.expect_with :rspec do |expectations| 23 | # This option will default to `true` in RSpec 4. It makes the `description` 24 | # and `failure_message` of custom matchers include text for helper methods 25 | # defined using `chain`, e.g.: 26 | # be_bigger_than(2).and_smaller_than(4).description 27 | # # => "be bigger than 2 and smaller than 4" 28 | # ...rather than: 29 | # # => "be bigger than 2" 30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 31 | end 32 | 33 | # rspec-mocks config goes here. You can use an alternate test double 34 | # library (such as bogus or mocha) by changing the `mock_with` option here. 35 | config.mock_with :rspec do |mocks| 36 | # Prevents you from mocking or stubbing a method that does not exist on 37 | # a real object. This is generally recommended, and will default to 38 | # `true` in RSpec 4. 39 | mocks.verify_partial_doubles = true 40 | end 41 | 42 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 43 | # have no way to turn it off -- the option exists only for backwards 44 | # compatibility in RSpec 3). It causes shared context metadata to be 45 | # inherited by the metadata hash of host groups and examples, rather than 46 | # triggering implicit auto-inclusion in groups with matching metadata. 47 | config.shared_context_metadata_behavior = :apply_to_host_groups 48 | 49 | # The settings below are suggested to provide a good initial experience 50 | # with RSpec, but feel free to customize to your heart's content. 51 | =begin 52 | # This allows you to limit a spec run to individual examples or groups 53 | # you care about by tagging them with `:focus` metadata. When nothing 54 | # is tagged with `:focus`, all examples get run. RSpec also provides 55 | # aliases for `it`, `describe`, and `context` that include `:focus` 56 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 57 | config.filter_run_when_matching :focus 58 | 59 | # Allows RSpec to persist some state between runs in order to support 60 | # the `--only-failures` and `--next-failure` CLI options. We recommend 61 | # you configure your source control system to ignore this file. 62 | config.example_status_persistence_file_path = "spec/examples.txt" 63 | 64 | # Limits the available syntax to the non-monkey patched syntax that is 65 | # recommended. For more details, see: 66 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 67 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 68 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 69 | config.disable_monkey_patching! 70 | 71 | # This setting enables warnings. It's recommended, but in some cases may 72 | # be too noisy due to issues in dependencies. 73 | config.warnings = true 74 | 75 | # Many RSpec users commonly either run the entire suite or an individual 76 | # file, and it's useful to allow more verbose output when running an 77 | # individual spec file. 78 | if config.files_to_run.one? 79 | # Use the documentation formatter for detailed output, 80 | # unless a formatter has already been configured 81 | # (e.g. via a command-line flag). 82 | config.default_formatter = "doc" 83 | end 84 | 85 | # Print the 10 slowest examples and example groups at the 86 | # end of the spec run, to help surface which specs are running 87 | # particularly slow. 88 | config.profile_examples = 10 89 | 90 | # Run specs in random order to surface order dependencies. If you find an 91 | # order dependency and want to debug it, you can fix the order by providing 92 | # the seed, which is printed after each run. 93 | # --seed 1234 94 | config.order = :random 95 | 96 | # Seed global randomization in this process using the `--seed` CLI option. 97 | # Setting this allows you to use `--seed` to deterministically reproduce 98 | # test failures related to randomization by passing the same `--seed` value 99 | # as the one that triggered the failure. 100 | Kernel.srand config.seed 101 | =end 102 | end 103 | -------------------------------------------------------------------------------- /lib/type_checker.rb: -------------------------------------------------------------------------------- 1 | require "ast" 2 | require "utils" 3 | 4 | class Object 5 | def do 6 | yield self 7 | end 8 | end 9 | 10 | module Types 11 | class Type 12 | def to_s 13 | self.class.name.split("::")[1] 14 | end 15 | end 16 | 17 | class Number < Type 18 | end 19 | 20 | class String < Type 21 | end 22 | 23 | class Boolean < Type 24 | end 25 | 26 | class Object < Type 27 | end 28 | 29 | class Any < Type 30 | end 31 | 32 | class Array < Type 33 | attr_reader :type 34 | 35 | def to_s 36 | "Array<#{type}>" 37 | end 38 | 39 | def initialize(type = Any) 40 | @type = type 41 | end 42 | end 43 | 44 | class Undefined < Type 45 | end 46 | 47 | class Function < Type 48 | attr_reader :args_type, :return_type 49 | 50 | def initialize(args_type, return_type) 51 | @args_type = args_type 52 | @return_type = return_type 53 | end 54 | end 55 | 56 | class Tuple < Type 57 | attr_reader :types 58 | 59 | def to_s 60 | "Tuple<#{types.map(&:to_s).join ", "}>" 61 | end 62 | 63 | def initialize(types) 64 | @types = types 65 | end 66 | end 67 | 68 | class Union < Type 69 | attr_reader :types 70 | 71 | def initialize(types) 72 | @types = types 73 | end 74 | end 75 | end 76 | 77 | class TypeChecker 78 | attr_reader :ast, :program_string, :pos, :stack 79 | 80 | def self.from(_self) 81 | self.new(_self.ast, _self.program_string, _self.pos, _self.stack) 82 | end 83 | 84 | def consume_steper!(checker_klass, *step_args) 85 | checker = checker_klass.from(self) 86 | expr_n = checker.step! *step_args 87 | @pos = checker.pos 88 | expr_n 89 | end 90 | 91 | def initialize(ast, program_string, pos = 0, stack = {}) 92 | @ast = ast 93 | @program_string = program_string 94 | @pos = pos 95 | @stack = stack 96 | @errors = [] 97 | end 98 | 99 | def step! 100 | for i in 0...@ast.size 101 | @ast[i] = consume_steper! StatementChecker, @ast[i] 102 | end 103 | assert_not_reached! if @has_errors 104 | @ast 105 | end 106 | 107 | def ignore! 108 | assert_not_reached! 109 | end 110 | 111 | def pos_to_line_and_col(pos) 112 | line, col, i = 1, 0, 0 113 | @program_string.each_char do |char| 114 | break if i == pos 115 | if char == "\n" 116 | col = 0 117 | line += 1 118 | else 119 | col += 1 120 | end 121 | i += 1 122 | end 123 | 124 | return line, col 125 | end 126 | 127 | def get_line_positions_to_print(node) 128 | previous_line = (@program_string[0..node.pos].rindex("\n") || -1) + 1 129 | previous_line = (@program_string[0...previous_line - 1].rindex("\n")) + 1 130 | 131 | next_line = @program_string[node.pos..].do { |s| 132 | i = s.index("\n") 133 | if i 134 | i + node.pos 135 | else 136 | @program_string.size 137 | end 138 | } 139 | 140 | return previous_line, next_line 141 | end 142 | 143 | def print_error(node, expected, got) 144 | previous_line, next_line = get_line_positions_to_print node 145 | line, col = pos_to_line_and_col node.pos 146 | puts "Type mismatch! [line:#{line}, col:#{col}]" 147 | lines = @program_string[previous_line..next_line].split("\n") 148 | puts "..." 149 | lines.each_with_index do |_line, index| 150 | puts "#{line - (lines.size - index) + 1} | #{_line}" 151 | end 152 | puts " #{" " * col}^ Expected #{expected}, got #{got}" 153 | 154 | abort 155 | end 156 | 157 | def print_failed_id_lookup(node) 158 | previous_line, next_line = get_line_positions_to_print node 159 | line, col = pos_to_line_and_col node.pos 160 | 161 | lines = @program_string[previous_line..next_line].split("\n") 162 | puts "..." 163 | lines.each_with_index do |_line, index| 164 | break if index == lines.size - 1 165 | puts "#{line - (lines.size - index) + 1} | #{_line}" 166 | end 167 | puts "#{line} | #{lines.last}" 168 | puts " #{" " * col}^" 169 | puts "Identifier \"#{node.value}\" not found! [line:#{line}, col:#{col}]" 170 | 171 | abort 172 | end 173 | 174 | def check_type_of_op(node, local_stack) 175 | lhs_type = type_of node.lhs, local_stack 176 | rhs_type = type_of node.rhs, local_stack 177 | op_fn_type = OPERATOR_TYPES[node.type.to_s] 178 | 179 | if !types_match?(op_fn_type.args_type, Types::Tuple.new([lhs_type, rhs_type])) 180 | print_error node, op_fn_type.args_type, Types::Tuple.new([lhs_type, rhs_type]) 181 | end 182 | 183 | op_fn_type.return_type 184 | end 185 | 186 | def type_of(node, temp_stack = {}) 187 | local_stack = { **@stack, **temp_stack } 188 | 189 | case node 190 | when AST::Int 191 | Types::Number.new 192 | when AST::Float 193 | Types::Number.new 194 | when AST::SimpleString 195 | Types::String.new 196 | when AST::Op 197 | check_type_of_op(node, local_stack) 198 | when AST::IdLookup 199 | if !local_stack[node.value] 200 | print_failed_id_lookup node 201 | end 202 | local_stack[node.value] 203 | else 204 | assert_not_reached! 205 | end 206 | end 207 | 208 | GLOBAL_CONSTRUCTORS = { 209 | "String" => Types::String.new, 210 | "Number" => Types::Number.new, 211 | "Boolean" => Types::Boolean.new, 212 | "Object" => Types::Object.new, 213 | "Array" => Types::Array.new, 214 | } 215 | 216 | CONSOLE_TYPES = { 217 | "log" => Types::Function.new( 218 | Types::Any.new, 219 | Types::Undefined.new 220 | ), 221 | } 222 | 223 | MATH_OPERATOR_TYPE = Types::Function.new( 224 | Types::Tuple.new([ 225 | Types::Number.new, 226 | Types::Number.new, 227 | ]), 228 | Types::Number.new 229 | ) 230 | 231 | OPERATOR_TYPES = { 232 | "+" => MATH_OPERATOR_TYPE, 233 | "*" => MATH_OPERATOR_TYPE, 234 | "-" => MATH_OPERATOR_TYPE, 235 | "/" => MATH_OPERATOR_TYPE, 236 | "**" => MATH_OPERATOR_TYPE, 237 | } 238 | end 239 | 240 | class StatementChecker < TypeChecker 241 | def step!(node) 242 | case node 243 | when AST::SimpleSchemaAssignment 244 | step_simple_schema_assignment node 245 | when AST::FnCall 246 | step_function_call node 247 | when AST::SimpleAssignment 248 | step_simple_assignment node 249 | when AST::SingleLineDefWithArgs 250 | step_single_line_def_with_args node 251 | else 252 | assert_not_reached! 253 | end 254 | end 255 | 256 | def fn_type_from_native_object(dot_node) 257 | assert { dot_node.lhs.value.is_a? String } 258 | case dot_node.lhs.value 259 | when "console" 260 | CONSOLE_TYPES[dot_node.rhs.value] 261 | end 262 | end 263 | 264 | def function_type(node) 265 | case node 266 | when AST::Dot 267 | fn_type_from_native_object(node) 268 | when AST::IdLookup 269 | @stack[node.value] 270 | else 271 | assert_not_reached! 272 | end 273 | end 274 | 275 | def args_type(args) 276 | arg_types = [] 277 | for arg in args 278 | arg_types.push type_of arg 279 | end 280 | Types::Tuple.new arg_types 281 | end 282 | 283 | def type_of_def_args(node_args) 284 | case node_args 285 | when AST::SimpleFnArgs 286 | Types::Tuple.new node_args.value.map { Types::Any.new } 287 | else 288 | assert_not_reached! 289 | end 290 | end 291 | 292 | def infer_type_of(id, expr) 293 | case expr 294 | when AST::Op 295 | type = OPERATOR_TYPES[expr.type.to_s] 296 | if expr.lhs.value == id 297 | type.args_type.types[0] 298 | elsif expr.rhs.value == id 299 | type.args_type.types[1] 300 | else 301 | assert_not_reached! 302 | end 303 | end 304 | end 305 | 306 | def step_single_line_def_with_args(node) 307 | args_type = Types::Tuple.new( 308 | node.args.value.map { |id| 309 | infer_type_of id.name, node.return_value 310 | } 311 | ) 312 | 313 | local_stack = node.args.value.map(&:name) 314 | .zip(args_type.types) 315 | .map { |name, type| [name, type] }.to_h 316 | return_type = type_of node.return_value, local_stack 317 | 318 | @stack[node.name] = Types::Function.new( 319 | args_type, 320 | return_type 321 | ) 322 | node 323 | end 324 | 325 | def step_function_call(node) 326 | fn_type = function_type node.expr 327 | call_args_type = args_type node.args 328 | 329 | if !types_match?(fn_type.args_type, call_args_type) 330 | print_error node, fn_type.args_type, call_args_type 331 | end 332 | 333 | node 334 | end 335 | 336 | def types_match?(into, from) 337 | return true if into.is_a?(Types::Any) || from.is_a?(Types::Any) 338 | 339 | case into 340 | when Types::Number 341 | from.is_a? Types::Number 342 | when Types::String 343 | from.is_a? Types::String 344 | when Types::Tuple 345 | return false if !from.is_a?(Types::Tuple) 346 | return false if from.types.size != into.types.size 347 | into.types 348 | .zip(from.types) 349 | .all? { |type_a, type_b| types_match? type_a, type_b } 350 | else 351 | binding.pry 352 | assert_not_reached! 353 | end 354 | end 355 | 356 | def step_simple_assignment(node) 357 | rhs_type = type_of node.expr 358 | 359 | assert { rhs_type } 360 | 361 | @stack[node.name] = rhs_type 362 | 363 | node 364 | end 365 | 366 | def step_simple_schema_assignment(node) 367 | schema_type = GLOBAL_CONSTRUCTORS[node.schema_name] 368 | ignore! if !schema_type 369 | rhs_type = type_of node.expr 370 | if !types_match?(rhs_type, schema_type) 371 | print_error node, schema_type, rhs_type 372 | end 373 | 374 | @stack[node.name] = rhs_type 375 | 376 | AST::SimpleAssignment.new(node.name, node.expr) 377 | end 378 | end 379 | -------------------------------------------------------------------------------- /lib/lexer.rb: -------------------------------------------------------------------------------- 1 | require "strscan" 2 | require "utils" 3 | 4 | module Lexer 5 | class Token 6 | attr_reader :raw_token, :start_pos, :end_pos, :type, :value, :captures 7 | def self.set_position!(pos) 8 | @@current_position = pos 9 | end 10 | 11 | def ==(other) 12 | type == other.type && value == other.value && captures == other.captures 13 | end 14 | 15 | def initialize(raw_token, type, value = nil, captures = nil) 16 | @raw_token = raw_token 17 | @start_pos = @@current_position 18 | @end_pos = @start_pos + @raw_token.size 19 | @type = type 20 | @value = value 21 | @captures = captures 22 | end 23 | 24 | def to_s 25 | s = "{ type: :#{type}" 26 | if value 27 | s += ", value: #{value}" 28 | end 29 | s + " }" 30 | end 31 | 32 | def value=(value) 33 | @value = value 34 | end 35 | 36 | def is?(type) 37 | type == @type 38 | end 39 | 40 | def is_not?(type) 41 | !is?(type) 42 | end 43 | 44 | def is_one_of?(*types) 45 | types.include? @type 46 | end 47 | 48 | def is_not_one_of?(*types) 49 | !is_one_of?(*types) 50 | end 51 | end 52 | 53 | def self.get_escaped_part_of_str(str, offset) 54 | capture_start = i = 0 55 | num_open_braces = 1 56 | while true 57 | case str[i] 58 | when "#" 59 | if str[i + 1] == "{" 60 | assert { false } 61 | end 62 | when "{" 63 | num_open_braces += 1 64 | when "}" 65 | num_open_braces -= 1 66 | if num_open_braces == 0 67 | return { start: offset + capture_start, 68 | end: offset + i, 69 | value: str[capture_start...i] } 70 | end 71 | end 72 | i += 1 73 | assert_not_reached if i >= str.size 74 | end 75 | end 76 | 77 | def self.tokenize(program) 78 | tokens = [] 79 | scanner = StringScanner.new(program) 80 | while true 81 | Token.set_position!(scanner.pos) 82 | case 83 | when scanner.eos? 84 | break 85 | when scanner.scan(/\s+/) 86 | next 87 | when scanner.scan(/#\{/) 88 | tokens.push Token.new(scanner.matched, :"#\{") 89 | when scanner.scan(/#([a-zA-Z]+)\b/) 90 | tokens.push Token.new(scanner.matched, :hash_id, scanner.matched[1..]) 91 | when scanner.scan(/#.*/) 92 | next 93 | when scanner.scan(/"/) 94 | str = "" 95 | captures = [] 96 | i = scanner.pos 97 | while i < program.size 98 | if program[i..i + 1] == "\#{" 99 | captures.push Lexer::get_escaped_part_of_str(program[i + 2..], i + 2 - scanner.pos) 100 | str += program[i..scanner.pos + captures.last[:end]] 101 | i = scanner.pos + captures.last[:end] + 1 102 | end 103 | if program[i] == '"' 104 | scanner.pos = i + 1 105 | break 106 | end 107 | str += program[i] 108 | i += 1 109 | end 110 | captures = captures.map do |capture| 111 | { **capture, 112 | tokens: Lexer::tokenize(capture[:value]) } 113 | end 114 | tokens.push Token.new("\"#{str}\"", :str_lit, str, captures) 115 | when scanner.scan(/\d+\.\d+/) 116 | tokens.push Token.new(scanner.matched, :float_lit, scanner.matched.to_f) 117 | when scanner.scan(/\d+/) 118 | tokens.push Token.new(scanner.matched, :int_lit, scanner.matched.to_i) 119 | when scanner.scan(/_/) 120 | tokens.push Token.new(scanner.matched, :identifier, "_") 121 | when scanner.scan(/(true|false)\b/) 122 | tokens.push Token.new(scanner.matched, :bool_lit, scanner.matched == "true") 123 | when scanner.scan(/null(?!\?)\b/) 124 | tokens.push Token.new(scanner.matched, :null) 125 | when scanner.scan(/undefined(?!\?)\b/) 126 | tokens.push Token.new(scanner.matched, :undefined) 127 | when scanner.scan(/this\b/) 128 | tokens.push Token.new(scanner.matched, :this) 129 | when scanner.scan(/new\b/) 130 | tokens.push Token.new(scanner.matched, :new) 131 | when scanner.scan(/static\b/) 132 | tokens.push Token.new(scanner.matched, :static) 133 | when scanner.scan(/do\b/) 134 | tokens.push Token.new(scanner.matched, :do) 135 | when scanner.scan(/function\b/) 136 | tokens.push Token.new(scanner.matched, :function) 137 | when scanner.scan(/class\b/) 138 | tokens.push Token.new(scanner.matched, :class) 139 | when scanner.scan(/get\b/) 140 | tokens.push Token.new(scanner.matched, :get) 141 | when scanner.scan(/for\b/) 142 | tokens.push Token.new(scanner.matched, :for) 143 | when scanner.scan(/of\b/) 144 | tokens.push Token.new(scanner.matched, :of) 145 | when scanner.scan(/case\b/) 146 | tokens.push Token.new(scanner.matched, :case) 147 | when scanner.scan(/when\b/) 148 | tokens.push Token.new(scanner.matched, :when) 149 | when scanner.scan(/component\b/) 150 | tokens.push Token.new(scanner.matched, :component) 151 | when scanner.scan(/in\b/) 152 | tokens.push Token.new(scanner.matched, :in) 153 | when scanner.scan(/instanceof\b/) 154 | tokens.push Token.new(scanner.matched, :instanceof) 155 | when scanner.scan(/if\b/) 156 | tokens.push Token.new(scanner.matched, :if) 157 | when scanner.scan(/unless\b/) 158 | tokens.push Token.new(scanner.matched, :unless) 159 | when scanner.scan(/else\b/) 160 | tokens.push Token.new(scanner.matched, :else) 161 | when scanner.scan(/end\b/) 162 | tokens.push Token.new(scanner.matched, :end) 163 | when scanner.scan(/enum\b/) 164 | tokens.push Token.new(scanner.matched, :enum) 165 | when scanner.scan(/<\//) 166 | tokens.push Token.new(scanner.matched, :"/) 168 | tokens.push Token.new(scanner.matched, :"=>") 169 | when scanner.scan(/!==/) 170 | tokens.push Token.new(scanner.matched, :"!==") 171 | when scanner.scan(/===/) 172 | tokens.push Token.new(scanner.matched, :"===") 173 | when scanner.scan(/==/) 174 | tokens.push Token.new(scanner.matched, :"==") 175 | when scanner.scan(/\!/) 176 | tokens.push Token.new(scanner.matched, :not) 177 | when scanner.scan(/@/) 178 | tokens.push Token.new(scanner.matched, :"@") 179 | when scanner.scan(/\+=/) 180 | tokens.push Token.new(scanner.matched, :"+=") 181 | when scanner.scan(/\|\|=/) 182 | tokens.push Token.new(scanner.matched, :"||=") 183 | when scanner.scan(/:=/) 184 | tokens.push Token.new(scanner.matched, :assign) 185 | when scanner.scan(/\(/) 186 | tokens.push Token.new(scanner.matched, :open_paren) 187 | when scanner.scan(/\)/) 188 | tokens.push Token.new(scanner.matched, :close_paren) 189 | when scanner.scan(/\{/) 190 | tokens.push Token.new(scanner.matched, :"{") 191 | when scanner.scan(/\}/) 192 | tokens.push Token.new(scanner.matched, :"}") 193 | when scanner.scan(/\[/) 194 | tokens.push Token.new(scanner.matched, :"[") 195 | when scanner.scan(/\]/) 196 | tokens.push Token.new(scanner.matched, :"]") 197 | when scanner.scan(/\.\.\./) 198 | tokens.push Token.new(scanner.matched, :"...") 199 | when scanner.scan(/\.\./) 200 | tokens.push Token.new(scanner.matched, :"..") 201 | when scanner.scan(/\?\./) 202 | tokens.push Token.new(scanner.matched, :"?.") 203 | when scanner.scan(/::/) 204 | tokens.push Token.new(scanner.matched, :"::") 205 | when scanner.scan(/\./) 206 | tokens.push Token.new(scanner.matched, :dot) 207 | when scanner.scan(/,/) 208 | tokens.push Token.new(scanner.matched, :comma) 209 | when scanner.scan(/\*/) 210 | tokens.push Token.new(scanner.matched, :*) 211 | when scanner.scan(/\//) 212 | tokens.push Token.new(scanner.matched, :/) 213 | when scanner.scan(/\+/) 214 | tokens.push Token.new(scanner.matched, :+) 215 | when scanner.scan(/\>=/) 216 | tokens.push Token.new(scanner.matched, :">=") 217 | when scanner.scan(/<=/) 218 | tokens.push Token.new(scanner.matched, :"<=") 219 | when scanner.scan(/\>/) 220 | tokens.push Token.new(scanner.matched, :>) 221 | when scanner.scan(/= this.str.length; 32 | } 33 | } 34 | 35 | class Token { 36 | constructor(value) { 37 | this.value = value; 38 | } 39 | is(TokenType) { 40 | return this instanceof TokenType; 41 | } 42 | } 43 | class ValueToken extends Token {} 44 | class IdentifierToken extends Token {} 45 | class OpenBrace extends Token {} 46 | class CloseBrace extends Token {} 47 | class Ampersand extends Token {} 48 | class PseudoClass extends Token {} 49 | class PseudoElement extends Token {} 50 | class Dot extends Token {} 51 | class Comma extends Token {} 52 | 53 | const tokenizeCSS = (str) => { 54 | const tokens = []; 55 | const scanner = new StringScanner(str); 56 | while (!scanner.is_end_of_str()) { 57 | if (scanner.scan(/\s+/)) { 58 | continue; 59 | } else if (scanner.scan(/&/)) { 60 | tokens.push(new Ampersand()); 61 | } else if (scanner.scan(/\./)) { 62 | tokens.push(new Dot()); 63 | } else if (scanner.scan(/\,/)) { 64 | tokens.push(new Comma()); 65 | } else if (scanner.scan(/::([a-z]+)/)) { 66 | tokens.push(new PseudoElement(scanner.groups[0])); 67 | } else if (scanner.scan(/:([a-z]+)/)) { 68 | tokens.push(new PseudoClass(scanner.groups[0])); 69 | } else if (scanner.scan(/:(\s)*(.*);/)) { 70 | tokens.push(new ValueToken(scanner.groups[1])); 71 | } else if (scanner.scan(/([a-z][a-z1-9\-]*|\*)/)) { 72 | tokens.push(new IdentifierToken(scanner.matched)); 73 | } else if (scanner.scan(/\{/)) { 74 | tokens.push(new OpenBrace()); 75 | } else if (scanner.scan(/\}/)) { 76 | tokens.push(new CloseBrace()); 77 | } else { 78 | throw new NotReached([scanner.index, scanner.rest_of_str()]); 79 | } 80 | } 81 | return tokens; 82 | }; 83 | 84 | class TokenMismatch extends Error { 85 | constructor(expected, got) { 86 | super(`expected - ${expected.name}, got - ${got.constructor.name}`); 87 | } 88 | } 89 | 90 | class AstNode { 91 | is(NodeType) { 92 | return this instanceof NodeType; 93 | } 94 | } 95 | class RuleNode extends AstNode { 96 | constructor(name, value) { 97 | super(); 98 | this.name = name; 99 | this.value = value; 100 | } 101 | } 102 | class ChildSelectorNode extends AstNode { 103 | constructor(tag_name, rules) { 104 | super(); 105 | this.tag_name = tag_name; 106 | this.rules = rules; 107 | } 108 | } 109 | 110 | class PseudoSelectorNode extends AstNode { 111 | constructor(pseudo_class, rules) { 112 | super(); 113 | this.pseudo_class = pseudo_class; 114 | this.rules = rules; 115 | } 116 | } 117 | 118 | class PseudoElementNode extends AstNode { 119 | constructor(pseudo_element, rules) { 120 | super(); 121 | this.pseudo_element = pseudo_element; 122 | this.rules = rules; 123 | } 124 | } 125 | 126 | class ClassSelectorNode extends AstNode { 127 | constructor(class_name, rules) { 128 | super(); 129 | this.class_name = class_name; 130 | this.rules = rules; 131 | } 132 | } 133 | 134 | class MultipleIdentifierNode extends AstNode { 135 | constructor(first_tag_name, second_tag_name, rules) { 136 | super(); 137 | this.first_tag_name = first_tag_name; 138 | this.second_tag_name = second_tag_name; 139 | this.rules = rules; 140 | } 141 | } 142 | 143 | class CSSParser { 144 | constructor(tokens, index = 0, end_token = null) { 145 | this.tokens = tokens; 146 | this.index = index; 147 | this.end_token = end_token; 148 | } 149 | 150 | get current() { 151 | return this.tokens[this.index]; 152 | } 153 | 154 | get peek() { 155 | return this.tokens[this.index + 1]; 156 | } 157 | 158 | get peek_two() { 159 | return this.tokens[this.index + 2]; 160 | } 161 | 162 | clone(end_token = null) { 163 | return new CSSParser(this.tokens, this.index, end_token || this.end_token); 164 | } 165 | 166 | consume_clone(parser) { 167 | this.index = parser.index; 168 | } 169 | 170 | consume(TokenClass) { 171 | if (!(this.current instanceof TokenClass)) 172 | throw new TokenMismatch(TokenClass, this.current); 173 | const token = this.current; 174 | this.index += 1; 175 | return token; 176 | } 177 | 178 | can_parse() { 179 | if (this.index >= this.tokens.length) return false; 180 | if (!this.end_token) return true; 181 | return !this.current.is(this.end_token); 182 | } 183 | 184 | parse() { 185 | const ast = []; 186 | while (this.can_parse()) { 187 | if (this.current.is(Dot) && this.peek?.is(IdentifierToken)) { 188 | ast.push(this.parse_class_selector()); 189 | } else if ( 190 | this.current.is(IdentifierToken) && 191 | this.peek?.is(Comma) && 192 | this.peek_two?.is(IdentifierToken) 193 | ) { 194 | ast.push(this.parse_multiple_identifier()); 195 | } else if (this.current.is(Ampersand) && this.peek?.is(PseudoClass)) { 196 | ast.push(this.parse_child_pseudo_class_selector()); 197 | } else if (this.current.is(Ampersand) && this.peek?.is(PseudoElement)) { 198 | ast.push(this.parse_child_pseudo_element_selector()); 199 | } else if (this.current.is(IdentifierToken) && this.peek?.is(OpenBrace)) { 200 | ast.push(this.parse_child_selector()); 201 | } else if (this.current.is(IdentifierToken)) { 202 | ast.push(this.parse_rule()); 203 | } else { 204 | throw new NotReached(this.current); 205 | } 206 | } 207 | return ast; 208 | } 209 | 210 | parse_class_selector() { 211 | this.consume(Dot); 212 | const { value: class_name } = this.consume(IdentifierToken); 213 | this.consume(OpenBrace); 214 | const cloned_parser = this.clone(CloseBrace); 215 | const rules = cloned_parser.parse(); 216 | this.consume_clone(cloned_parser); 217 | this.consume(CloseBrace); 218 | return new ClassSelectorNode(class_name, rules); 219 | } 220 | 221 | parse_child_pseudo_class_selector() { 222 | this.consume(Ampersand); 223 | const { value: selector } = this.consume(PseudoClass); 224 | this.consume(OpenBrace); 225 | const cloned_parser = this.clone(CloseBrace); 226 | const rules = cloned_parser.parse(); 227 | this.consume_clone(cloned_parser); 228 | this.consume(CloseBrace); 229 | return new PseudoSelectorNode(selector, rules); 230 | } 231 | 232 | parse_multiple_identifier() { 233 | const { value: first_tag_name } = this.consume(IdentifierToken); 234 | this.consume(Comma); 235 | const { value: second_tag_name } = this.consume(IdentifierToken); 236 | this.consume(OpenBrace); 237 | const cloned_parser = this.clone(CloseBrace); 238 | const rules = cloned_parser.parse(); 239 | this.consume_clone(cloned_parser); 240 | this.consume(CloseBrace); 241 | return new MultipleIdentifierNode(first_tag_name, second_tag_name, rules); 242 | } 243 | 244 | parse_child_pseudo_element_selector() { 245 | this.consume(Ampersand); 246 | const { value: pseudo_element } = this.consume(PseudoElement); 247 | this.consume(OpenBrace); 248 | const cloned_parser = this.clone(CloseBrace); 249 | const rules = cloned_parser.parse(); 250 | this.consume_clone(cloned_parser); 251 | this.consume(CloseBrace); 252 | return new PseudoElementNode(pseudo_element, rules); 253 | } 254 | 255 | parse_child_selector() { 256 | const { value: tag_name } = this.consume(IdentifierToken); 257 | this.consume(OpenBrace); 258 | const cloned_parser = this.clone(CloseBrace); 259 | const rules = cloned_parser.parse(); 260 | this.consume_clone(cloned_parser); 261 | this.consume(CloseBrace); 262 | return new ChildSelectorNode(tag_name, rules); 263 | } 264 | 265 | parse_rule() { 266 | const { value: name } = this.consume(IdentifierToken); 267 | const { value } = this.consume(ValueToken); 268 | return new RuleNode(name, value); 269 | } 270 | } 271 | 272 | class CSSCompiler { 273 | constructor(ast, parent_selector, top_level = true) { 274 | this.ast = ast; 275 | this.parent_selector = parent_selector; 276 | this.top_level = top_level; 277 | } 278 | 279 | base_rules_end() { 280 | if (this.top_level) { 281 | return "}\n"; 282 | } else { 283 | return ""; 284 | } 285 | } 286 | 287 | eval() { 288 | let rules_str = `${this.parent_selector} {\n`; 289 | const [rules, sub_rules] = this.eval_rules_and_sub_rules(); 290 | return [rules_str + rules, ...sub_rules]; 291 | } 292 | 293 | eval_rules_and_sub_rules() { 294 | let rules = ""; 295 | const rule_nodes = this.ast.filter((node) => node instanceof RuleNode); 296 | for (let node of rule_nodes) { 297 | rules += ` ${this.eval_rule(node)}\n`; 298 | } 299 | rules += this.base_rules_end(); 300 | 301 | let sub_rules = []; 302 | const sub_rule_nodes = this.ast.filter( 303 | (node) => !rule_nodes.includes(node) 304 | ); 305 | for (let node of sub_rule_nodes) { 306 | switch (node.constructor) { 307 | case ClassSelectorNode: 308 | sub_rules = sub_rules.concat(this.eval_class_selector(node)); 309 | break; 310 | case PseudoSelectorNode: 311 | sub_rules = sub_rules.concat(this.eval_pseudo_class_selector(node)); 312 | break; 313 | case PseudoElementNode: 314 | sub_rules = sub_rules.concat(this.eval_pseudo_element_selector(node)); 315 | break; 316 | case ChildSelectorNode: 317 | sub_rules = sub_rules.concat(this.eval_child_selector(node)); 318 | break; 319 | default: 320 | throw new NotReached(); 321 | } 322 | } 323 | return [rules, sub_rules]; 324 | } 325 | 326 | eval_rule({ name, value }) { 327 | return `${name}: ${value};`; 328 | } 329 | 330 | eval_class_selector({ class_name, rules: ast }) { 331 | if (!class_name) throw new NotReached(); 332 | const selector = `${this.parent_selector} .${class_name}`; 333 | let output = `${selector} {\n`; 334 | const [rules, sub_rules] = new CSSCompiler( 335 | ast, 336 | selector, 337 | false 338 | ).eval_rules_and_sub_rules(); 339 | output += rules; 340 | output += "}\n"; 341 | 342 | return [output, ...sub_rules]; 343 | } 344 | 345 | eval_pseudo_class_selector({ pseudo_class, rules: ast }) { 346 | if (!pseudo_class) throw new NotReached(); 347 | const selector = `${this.parent_selector}:${pseudo_class}`; 348 | let output = `${selector} {\n`; 349 | const [rules, sub_rules] = new CSSCompiler( 350 | ast, 351 | selector, 352 | false 353 | ).eval_rules_and_sub_rules(); 354 | output += rules; 355 | output += "}\n"; 356 | 357 | return [output, ...sub_rules]; 358 | } 359 | 360 | eval_pseudo_element_selector({ pseudo_element, rules: ast }) { 361 | if (!pseudo_element) throw new NotReached(); 362 | const selector = `${this.parent_selector}::${pseudo_element}`; 363 | let output = `${selector} {\n`; 364 | const [rules, sub_rules] = new CSSCompiler( 365 | ast, 366 | selector, 367 | false 368 | ).eval_rules_and_sub_rules(); 369 | output += rules; 370 | output += "}\n"; 371 | 372 | return [output, ...sub_rules]; 373 | } 374 | 375 | eval_child_selector({ tag_name, rules: ast }) { 376 | const selector = `${this.parent_selector} ${tag_name}`; 377 | let output = `${selector} {\n`; 378 | const [rules, sub_rules] = new CSSCompiler( 379 | ast, 380 | selector, 381 | false 382 | ).eval_rules_and_sub_rules(); 383 | output += rules; 384 | output += "}\n"; 385 | 386 | return [output, ...sub_rules]; 387 | } 388 | } 389 | 390 | function compile_css(style, tag_name) { 391 | const tokens = tokenizeCSS(style); 392 | const ast = new CSSParser(tokens).parse(); 393 | return new CSSCompiler(ast, tag_name).eval(); 394 | } 395 | 396 | function parse_css(style) { 397 | const tokens = tokenizeCSS(style); 398 | return new CSSParser(tokens).parse(); 399 | } 400 | -------------------------------------------------------------------------------- /lib/ast.rb: -------------------------------------------------------------------------------- 1 | require "pry" 2 | 3 | module AST 4 | class Node 5 | attr_reader :start_pos, :end_pos 6 | 7 | def initialize(start_pos, end_pos) 8 | @start_pos = start_pos 9 | @end_pos = end_pos 10 | end 11 | 12 | def declare? 13 | false 14 | end 15 | 16 | def schema? 17 | false 18 | end 19 | 20 | def to_h 21 | hash = { 22 | "klass" => self.class.to_s, 23 | } 24 | instance_variables.each do |var| 25 | node = instance_variable_get(var) 26 | value = if node.is_a? Node 27 | node.to_h 28 | elsif node.is_a?(Array) && node.all? { |n| n.is_a?(Node) } 29 | node.map(&:to_h) 30 | else 31 | node 32 | end 33 | hash[var.to_s.delete("@")] = value 34 | end 35 | hash 36 | end 37 | 38 | def is_not_one_of?(*klasses) 39 | !klasses.any? { |klass| is_a? klass } 40 | end 41 | 42 | def is_not_a?(klass) 43 | !is_a?(klass) 44 | end 45 | 46 | def start_pos=(val) 47 | @start_pos = val 48 | end 49 | 50 | def end_pos=(val) 51 | @end_pos = val 52 | end 53 | 54 | def captures 55 | [] 56 | end 57 | end 58 | 59 | class Primitive < Node 60 | attr_reader :value 61 | 62 | def initialize(value, start_pos, end_pos) 63 | @value = value 64 | @start_pos = start_pos 65 | @end_pos = end_pos 66 | end 67 | end 68 | 69 | class Int < Primitive 70 | end 71 | 72 | class Float < Primitive 73 | end 74 | 75 | class Bool < Primitive 76 | end 77 | 78 | class SimpleString < Primitive 79 | end 80 | 81 | class Expr < Node 82 | attr_reader :expr 83 | 84 | def initialize(expr, start_pos, end_pos) 85 | @expr = expr 86 | @start_pos = start_pos 87 | @end_pos = end_pos 88 | end 89 | end 90 | 91 | class ArrayLiteral < Expr 92 | def captures 93 | list = [] 94 | @value.each do |val| 95 | list.concat val.captures 96 | end 97 | list 98 | end 99 | end 100 | 101 | class ObjectLiteral < Expr 102 | end 103 | 104 | class NamedArg < Node 105 | attr_reader :name 106 | 107 | def initialize(name, start_pos, end_pos) 108 | @name = name 109 | @start_pos = start_pos 110 | @end_pos = end_pos 111 | end 112 | end 113 | 114 | class SimpleArg < NamedArg 115 | end 116 | 117 | class SpreadArg < NamedArg 118 | end 119 | 120 | class ThisSchemaArg < Node 121 | attr_reader :schema 122 | 123 | def initialize(schema, start_pos, end_pos) 124 | @schema = schema 125 | @start_pos = start_pos 126 | @end_pos = end_pos 127 | end 128 | end 129 | 130 | class SimpleSchemaArg < Node 131 | attr_reader :schema_name, :name 132 | 133 | def initialize(schema_name, name, start_pos, end_pos) 134 | @schema_name = schema_name 135 | @name = name 136 | @start_pos = start_pos 137 | @end_pos = end_pos 138 | end 139 | end 140 | 141 | class SimpleFnArgs < Node 142 | attr_reader :args 143 | 144 | def initialize(args, start_pos, end_pos) 145 | @args = args 146 | @start_pos = start_pos 147 | @end_pos = end_pos 148 | end 149 | end 150 | 151 | class ObjectEntry < Node 152 | attr_reader :key_name, :value 153 | 154 | def initialize(key_name, value, start_pos, end_pos) 155 | @key_name = key_name 156 | @value = value 157 | @start_pos = start_pos 158 | @end_pos = end_pos 159 | end 160 | end 161 | 162 | class SimpleObjectEntry < ObjectEntry 163 | end 164 | 165 | class ArrowMethodObjectEntry < ObjectEntry 166 | end 167 | 168 | class FunctionObjectEntry < ObjectEntry 169 | end 170 | 171 | class SpreadObjectEntry < Expr 172 | end 173 | 174 | class Not < Expr 175 | end 176 | 177 | class SchemaDefinition < Node 178 | attr_reader :name, :schema_expr 179 | 180 | def initialize(name, schema_expr, start_pos, end_pos) 181 | @name = name 182 | @schema_expr = schema_expr 183 | @start_pos = start_pos 184 | @end_pos = end_pos 185 | end 186 | end 187 | 188 | class SchemaInt < Node 189 | attr_reader :value 190 | 191 | def initialize(int_token) 192 | @value = int_token.value 193 | @start_pos = int_token.start_pos 194 | @end_pos = int_token.end_pos 195 | end 196 | 197 | def schema? 198 | true 199 | end 200 | end 201 | 202 | class SchemaObjectLiteral < Node 203 | attr_reader :properties 204 | 205 | def to_h 206 | props = [] 207 | for key, value in properties 208 | props.push [key, value.to_h] 209 | end 210 | props 211 | end 212 | 213 | def initialize(properties, start_pos, end_pos) 214 | @properties = properties 215 | @start_pos = start_pos 216 | @end_pos = end_pos 217 | end 218 | end 219 | 220 | class New < Node 221 | attr_reader :class_expr, :args 222 | 223 | def initialize(class_expr, args, start_pos, end_pos) 224 | @class_expr = class_expr 225 | @args = args 226 | @start_pos = start_pos 227 | @end_pos = end_pos 228 | end 229 | end 230 | 231 | class CaseFnPattern < Node 232 | attr_reader :this_pattern, :patterns, :body 233 | 234 | def initialize(this_pattern, patterns, body, start_pos, end_pos) 235 | @this_pattern = this_pattern 236 | @patterns = patterns 237 | @body = body 238 | @start_pos = start_pos 239 | @end_pos = end_pos 240 | end 241 | end 242 | 243 | class ArrayComprehension < Node 244 | attr_reader :expr, :variable, :array_expr, :if_expr 245 | 246 | def initialize(expr, variable, array_expr, if_expr, start_pos, end_pos) 247 | @expr = expr 248 | @variable = variable 249 | @array_expr = array_expr 250 | @if_expr = if_expr 251 | @start_pos = start_pos 252 | @end_pos = end_pos 253 | end 254 | end 255 | 256 | class SingleLineBindFunctionDefinition < Node 257 | attr_reader :object_name, :function_name, :args, :return_expr 258 | 259 | def initialize(object_name, function_name, args, return_expr, start_pos, end_pos) 260 | @object_name = object_name 261 | @function_name = function_name 262 | @args = args 263 | @return_expr = return_expr 264 | @start_pos = start_pos 265 | @end_pos = end_pos 266 | end 267 | end 268 | 269 | class DefaultAssignment < Node 270 | attr_reader :lhs, :expr 271 | 272 | def initialize(lhs, expr, start_pos, end_pos) 273 | @lhs = lhs 274 | @expr = expr 275 | @start_pos = start_pos 276 | @end_pos = end_pos 277 | end 278 | end 279 | 280 | class PlusAssignment < Node 281 | attr_reader :lhs, :expr 282 | 283 | def initialize(lhs, expr, start_pos, end_pos) 284 | @lhs = lhs 285 | @expr = expr 286 | @start_pos = start_pos 287 | @end_pos = end_pos 288 | end 289 | end 290 | 291 | class SimpleWhen < Node 292 | attr_reader :expr, :body 293 | 294 | def initialize(expr, body, start_pos, end_pos) 295 | @expr = expr 296 | @body = body 297 | @start_pos = start_pos 298 | @end_pos = end_pos 299 | end 300 | end 301 | 302 | class CaseElse < Node 303 | attr_reader :body 304 | 305 | def initialize(body, start_pos, end_pos) 306 | @body = body 307 | @start_pos = start_pos 308 | @end_pos = end_pos 309 | end 310 | end 311 | 312 | class EmptyCaseExpr < Node 313 | attr_reader :cases 314 | 315 | def initialize(cases, start_pos, end_pos) 316 | @cases = cases 317 | @start_pos = start_pos 318 | @end_pos = end_pos 319 | end 320 | end 321 | 322 | class MultiLineBindFunctionDefinition < Node 323 | attr_reader :object_name, :function_name, :args, :body 324 | 325 | def initialize(object_name, function_name, args, body, start_pos, end_pos) 326 | @object_name = object_name 327 | @function_name = function_name 328 | @args = args 329 | @body = body 330 | @start_pos = start_pos 331 | @end_pos = end_pos 332 | end 333 | end 334 | 335 | class CaseFunctionDefinition < Node 336 | attr_reader :name, :patterns 337 | 338 | def initialize(name, patterns, start_pos, end_pos) 339 | @name = name 340 | @patterns = patterns 341 | @start_pos = start_pos 342 | @end_pos = end_pos 343 | end 344 | end 345 | 346 | class InstanceProperty < Node 347 | attr_reader :name, :expr 348 | 349 | def initialize(name, expr, start_pos, end_pos) 350 | @name = name 351 | @expr = expr 352 | @start_pos = start_pos 353 | @end_pos = end_pos 354 | end 355 | end 356 | 357 | class SpreadExpr < Expr 358 | end 359 | 360 | class OneLineGetter < Node 361 | attr_reader :name, :expr 362 | 363 | def initialize(name, expr, start_pos, end_pos) 364 | @name = name 365 | @expr = expr 366 | @start_pos = start_pos 367 | @end_pos = end_pos 368 | end 369 | end 370 | 371 | class DynamicLookup < Node 372 | attr_reader :lhs, :expr 373 | 374 | def initialize(lhs, expr, start_pos, end_pos) 375 | @lhs = lhs 376 | @expr = expr 377 | @start_pos = start_pos 378 | @end_pos = end_pos 379 | end 380 | end 381 | 382 | class Return < Expr 383 | end 384 | 385 | class If < Node 386 | attr_reader :cond, :pass, :branches 387 | 388 | def initialize(cond, pass, branches, start_pos, end_pos) 389 | @cond = cond 390 | @pass = pass 391 | @branches = branches 392 | @start_pos = start_pos 393 | @end_pos = end_pos 394 | end 395 | end 396 | 397 | class ElseIf < Node 398 | attr_reader :cond, :body 399 | 400 | def initialize(cond, body, start_pos, end_pos) 401 | @cond = cond 402 | @body = body 403 | @start_pos = start_pos 404 | @end_pos = end_pos 405 | end 406 | end 407 | 408 | class Else < Node 409 | attr_reader :body 410 | 411 | def initialize(body, start_pos, end_pos) 412 | @body = body 413 | @start_pos = start_pos 414 | @end_pos = end_pos 415 | end 416 | end 417 | 418 | class FnCall < Node 419 | attr_reader :args, :expr 420 | 421 | def initialize(args, return_expr_n, start_pos, end_pos) 422 | assert { args.is_a? Array } 423 | @args = args 424 | @expr = return_expr_n 425 | @start_pos = start_pos 426 | @end_pos = end_pos 427 | end 428 | end 429 | 430 | class SingleLineDefWithArgs < Node 431 | attr_reader :return_value, :name, :args 432 | 433 | def initialize(name, args, return_value, start_pos, end_pos) 434 | @name = name 435 | @args = args 436 | @return_value = return_value 437 | @start_pos = start_pos 438 | @end_pos = end_pos 439 | end 440 | end 441 | 442 | class SingleLineDefWithoutArgs < Node 443 | attr_reader :return_value, :name 444 | 445 | def initialize(name, return_value, start_pos, end_pos) 446 | @name = name 447 | @return_value = return_value 448 | @start_pos = start_pos 449 | @end_pos = end_pos 450 | end 451 | end 452 | 453 | class Null < Node 454 | end 455 | 456 | class NullSchema < Node 457 | def name 458 | nil 459 | end 460 | 461 | def schema? 462 | true 463 | end 464 | end 465 | 466 | class MultilineDefWithoutArgs < Node 467 | attr_reader :body, :name 468 | 469 | def initialize(name, body, start_pos, end_pos) 470 | @name = name 471 | @body = body 472 | @start_pos = start_pos 473 | @end_pos = end_pos 474 | end 475 | end 476 | 477 | class ConstructorWithoutArgs < MultilineDefWithoutArgs 478 | end 479 | 480 | class ShortHandConstructor < Node 481 | attr_reader :args 482 | 483 | def initialize(args, start_pos, end_pos) 484 | @args = args 485 | @start_pos = start_pos 486 | @end_pos = end_pos 487 | end 488 | end 489 | 490 | class SimpleConstructorArg < Node 491 | attr_reader :name 492 | 493 | def initialize(name, start_pos, end_pos) 494 | @name = name 495 | @start_pos = start_pos 496 | @end_pos = end_pos 497 | end 498 | end 499 | 500 | class DefaultConstructorArg < Node 501 | attr_reader :name, :expr 502 | 503 | def initialize(name, expr, start_pos, end_pos) 504 | @name = name 505 | @expr = expr 506 | @start_pos = start_pos 507 | @end_pos = end_pos 508 | end 509 | end 510 | 511 | class MultilineDefWithArgs < Node 512 | attr_reader :body, :args, :name 513 | 514 | def initialize(name, args, body, start_pos, end_pos) 515 | @name = name 516 | @args = args 517 | @body = body 518 | @start_pos = start_pos 519 | @end_pos = end_pos 520 | end 521 | end 522 | 523 | class StaticMethod < MultilineDefWithArgs 524 | end 525 | 526 | class ConstructorWithArgs < MultilineDefWithArgs 527 | end 528 | 529 | class ShortFnWithArgs < Node 530 | attr_reader :return_expr, :args 531 | 532 | def initialize(args, return_expr, start_pos, end_pos) 533 | @args = args 534 | @return_expr = return_expr 535 | @start_pos = start_pos 536 | @end_pos = end_pos 537 | end 538 | end 539 | 540 | class ShortFn < Node 541 | attr_reader :return_expr 542 | 543 | def initialize(return_expr, start_pos, end_pos) 544 | @return_expr = return_expr 545 | @start_pos = start_pos 546 | @end_pos = end_pos 547 | end 548 | end 549 | 550 | class MultiLineArrowFnWithArgs < Node 551 | attr_reader :args, :body 552 | 553 | def initialize(args, body, start_pos, end_pos) 554 | @args = args 555 | @body = body 556 | @start_pos = start_pos 557 | @end_pos = end_pos 558 | end 559 | end 560 | 561 | class This < Node 562 | end 563 | 564 | class Bind < Node 565 | attr_reader :lhs, :function, :args 566 | 567 | def initialize(lhs, function, args) 568 | @lhs = lhs 569 | @function = function 570 | @args = args 571 | end 572 | end 573 | 574 | class OptionalChain < Node 575 | attr_reader :lhs, :property 576 | 577 | def initialize(lhs, property, start_pos, end_pos) 578 | @lhs = lhs 579 | @property = property 580 | @start_pos = start_pos 581 | @end_pos = end_pos 582 | end 583 | end 584 | 585 | class DotAssignment < Node 586 | attr_reader :lhs, :expr 587 | 588 | def initialize(lhs, expr, start_pos, end_pos) 589 | @lhs = lhs 590 | @expr = expr 591 | @start_pos = start_pos 592 | @end_pos = end_pos 593 | end 594 | end 595 | 596 | class Class < Node 597 | attr_reader :name, :parent_class, :entries 598 | 599 | def initialize(name, parent_class, entries, start_pos, end_pos) 600 | @name = name 601 | @parent_class = parent_class 602 | @entries = entries 603 | @start_pos = start_pos 604 | @end_pos = end_pos 605 | end 606 | end 607 | 608 | class SingleLineArrowFnWithoutArgs < Node 609 | attr_reader :return_expr 610 | 611 | def initialize(return_expr, start_pos, end_pos) 612 | @return_expr = return_expr 613 | @start_pos = start_pos 614 | @end_pos = end_pos 615 | end 616 | end 617 | 618 | class EscapedElementExpr < Expr 619 | end 620 | 621 | class SimpleElement < Node 622 | attr_reader :name, :children 623 | 624 | def initialize(name, children, start_pos, end_pos) 625 | @name = name 626 | @children = children 627 | @start_pos = start_pos 628 | @end_pos = end_pos 629 | end 630 | end 631 | 632 | class BodyComponentWithoutAttrs < Node 633 | attr_reader :name, :constructor_body, :expr 634 | 635 | def initialize(name, constructor_body, expr, start_pos, end_pos) 636 | @name = name 637 | @constructor_body = constructor_body 638 | @expr = expr 639 | @start_pos = start_pos 640 | @end_pos = end_pos 641 | end 642 | end 643 | 644 | class ExprComponentWithAttributes < Node 645 | attr_reader :name, :attributes, :expr 646 | 647 | def initialize(name, attributes, expr, start_pos, end_pos) 648 | @name = name 649 | @attributes = attributes 650 | @expr = expr 651 | @start_pos = start_pos 652 | @end_pos = end_pos 653 | end 654 | end 655 | 656 | class ExprComponent < Node 657 | attr_reader :name, :expr 658 | 659 | def initialize(name, expr, start_pos, end_pos) 660 | @name = name 661 | @expr = expr 662 | @start_pos = start_pos 663 | @end_pos = end_pos 664 | end 665 | end 666 | 667 | class SingleLineArrowFnWithArgs < Node 668 | attr_reader :args, :return_expr 669 | 670 | def initialize(args, return_expr, start_pos, end_pos) 671 | @args = args 672 | @return_expr = return_expr 673 | @start_pos = start_pos 674 | @end_pos = end_pos 675 | end 676 | end 677 | 678 | class SingleLineArrowFnWithOneArg < Node 679 | attr_reader :arg, :return_expr 680 | 681 | def initialize(arg, return_expr, start_pos, end_pos) 682 | @arg = arg 683 | @return_expr = return_expr 684 | @start_pos = start_pos 685 | @end_pos = end_pos 686 | end 687 | end 688 | 689 | class AnonIdLookup < Node 690 | end 691 | 692 | class IdLookup < Primitive 693 | def captures 694 | [] 695 | end 696 | end 697 | 698 | class SchemaCapture < Node 699 | attr_reader :name 700 | 701 | def initialize(name, start_pos, end_pos) 702 | @name = name 703 | @start_pos = start_pos 704 | @end_pos = end_pos 705 | end 706 | 707 | def captures 708 | [name] 709 | end 710 | end 711 | 712 | class SimpleForOfLoop < Node 713 | attr_reader :iter_name, :arr_expr, :body 714 | 715 | def initialize(iter_name, arr_expr, body, start_pos, end_pos) 716 | @iter_name = iter_name 717 | @arr_expr = arr_expr 718 | @body = body 719 | @start_pos = start_pos 720 | @end_pos = end_pos 721 | end 722 | end 723 | 724 | class ForOfObjDeconstructLoop < Node 725 | attr_reader :iter_properties, :arr_expr, :body 726 | 727 | def initialize(iter_properties, arr_expr, body, start_pos, end_pos) 728 | @iter_properties = iter_properties 729 | @arr_expr = arr_expr 730 | @body = body 731 | @start_pos = start_pos 732 | @end_pos = end_pos 733 | end 734 | end 735 | 736 | class Op < Node 737 | attr_reader :lhs, :type, :rhs 738 | 739 | def initialize(lhs, type, rhs, start_pos, end_pos) 740 | @lhs = lhs 741 | @type = type 742 | @rhs = rhs 743 | @start_pos = start_pos 744 | @end_pos = end_pos 745 | end 746 | end 747 | 748 | class OneLineEnum < Node 749 | attr_reader :name, :variant_names 750 | 751 | def initialize(name, variant_names, start_pos, end_pos) 752 | @name = name 753 | @variant_names = variant_names 754 | @start_pos = start_pos 755 | @end_pos = end_pos 756 | end 757 | end 758 | 759 | class EnumExpr < Node 760 | attr_reader :enum_name, :variant_name 761 | 762 | def initialize(enum_name, variant_name, start_pos, end_pos) 763 | @enum_name = enum_name 764 | @variant_name = variant_name 765 | @start_pos = start_pos 766 | @end_pos = end_pos 767 | end 768 | end 769 | 770 | class Range < Node 771 | attr_reader :lhs, :rhs 772 | 773 | def initialize(lhs, rhs, start_pos, end_pos) 774 | @lhs = lhs 775 | @rhs = rhs 776 | @start_pos = start_pos 777 | @end_pos = end_pos 778 | end 779 | end 780 | 781 | class Dot < Op 782 | end 783 | 784 | class SchemaUnion < Node 785 | attr_reader :schema_exprs 786 | 787 | def initialize(schema_exprs, start_pos, end_pos) 788 | @schema_exprs = schema_exprs 789 | @start_pos = start_pos 790 | @end_pos = end_pos 791 | end 792 | end 793 | 794 | class Empty < Node 795 | def initialize 796 | end 797 | end 798 | 799 | class SchemaIntersect < Node 800 | attr_reader :schema_exprs 801 | 802 | def initialize(schema_exprs, start_pos, end_pos) 803 | @schema_exprs = schema_exprs 804 | @start_pos = start_pos 805 | @end_pos = end_pos 806 | end 807 | end 808 | 809 | class Assign < Node 810 | attr_reader :name, :expr 811 | 812 | def initialize(name, return_expr_n, start_pos, end_pos) 813 | @name = name 814 | @expr = return_expr_n 815 | @start_pos = start_pos 816 | @end_pos = end_pos 817 | end 818 | 819 | def declare? 820 | true 821 | end 822 | end 823 | 824 | class Await < Expr 825 | end 826 | 827 | # Schema(a) := b 828 | class SimpleSchemaAssignment < Assign 829 | attr_reader :schema_name 830 | 831 | def initialize(schema_name, name, expr, start_pos, end_pos) 832 | @schema_name = schema_name 833 | @name = name 834 | @expr = expr 835 | @start_pos = start_pos 836 | @end_pos = end_pos 837 | end 838 | end 839 | 840 | # a := 1 841 | class SimpleAssignment < Assign 842 | attr_reader :name, :expr 843 | 844 | def initialize(name, return_expr_n, start_pos, end_pos) 845 | @name = name 846 | @expr = return_expr_n 847 | @start_pos = start_pos 848 | @end_pos = end_pos 849 | end 850 | 851 | def captures 852 | [name] 853 | end 854 | end 855 | 856 | # a = 1 857 | class SimpleReassignment < Node 858 | attr_reader :name, :expr 859 | 860 | def initialize(name, return_expr_n, start_pos, end_pos) 861 | @name = name 862 | @expr = return_expr_n 863 | @start_pos = start_pos 864 | @end_pos = end_pos 865 | end 866 | 867 | def declare? 868 | false 869 | end 870 | 871 | def captures 872 | [name] 873 | end 874 | end 875 | 876 | # a := 877 | class ImcompleteSimpleAssignment < Assign 878 | attr_reader :name 879 | 880 | def initialize(name, start_pos, end_pos) 881 | @name = name 882 | @start_pos = start_pos 883 | @end_pos = end_pos 884 | end 885 | 886 | def captures 887 | [name] 888 | end 889 | end 890 | 891 | class SimpleForInLoop < Node 892 | attr_reader :variable, :object_expr, :body 893 | 894 | def initialize(variable, object_expr, body, start_pos, end_pos) 895 | @variable = variable 896 | @object_expr = object_expr 897 | @body = body 898 | @start_pos = start_pos 899 | @end_pos = end_pos 900 | end 901 | end 902 | 903 | class ArrayAssignment < Node 904 | attr_reader :variables, :expr 905 | 906 | def initialize(variables, expr, start_pos, end_pos) 907 | @variables = variables 908 | @expr = expr 909 | @start_pos = start_pos 910 | @end_pos = end_pos 911 | end 912 | 913 | def declare? 914 | true 915 | end 916 | 917 | def captures 918 | @variables 919 | end 920 | end 921 | end 922 | -------------------------------------------------------------------------------- /lib/compiler.rb: -------------------------------------------------------------------------------- 1 | require "utils" 2 | require "pry" 3 | require "parser" 4 | 5 | class Compiler 6 | def initialize(ast, indent = 0, fn_arg_names: [], bundle_std_lib: false, is_class_definition: false) 7 | @ast = ast 8 | @indent = indent 9 | @fn_arg_names = fn_arg_names 10 | @bundle_std_lib = bundle_std_lib 11 | @is_class_definition = is_class_definition 12 | end 13 | 14 | def eval 15 | program = "" 16 | program += std_lib if @bundle_std_lib 17 | program += eval_assignment_declarations 18 | # program += collapse_function_overloading 19 | @ast.each do |statement| 20 | program += " " * @indent 21 | program += eval_expr(statement) 22 | program += ";" << "\n" 23 | end 24 | program.rstrip 25 | end 26 | 27 | def eval_without_variable_declarations 28 | program = "" 29 | # program += collapse_function_overloading 30 | @ast.each do |statement| 31 | program += " " * @indent 32 | program += eval_expr(statement) 33 | program += ";" << "\n" 34 | end 35 | program.rstrip 36 | end 37 | 38 | private 39 | 40 | def std_lib 41 | if ARGV[1] == "-s" 42 | "" 43 | else 44 | [symbols, schema_lib, range, pea_std, pea_array].join("\n") + "\n" 45 | end 46 | end 47 | 48 | def pea_std 49 | file_str = File.read(File.dirname(__FILE__) + "/pea_std_lib/std.pea") 50 | tokens = Lexer.tokenize file_str 51 | ast = Parser.new(tokens, file_str).parse! 52 | Compiler.new(ast).eval 53 | end 54 | 55 | def pea_array 56 | file_str = File.read(File.dirname(__FILE__) + "/pea_std_lib/array.pea") 57 | tokens = Lexer.tokenize file_str 58 | ast = Parser.new(tokens, file_str).parse! 59 | Compiler.new(ast).eval 60 | end 61 | 62 | def range 63 | File.read(File.dirname(__FILE__) + "/pea_std_lib/range.js") 64 | end 65 | 66 | def schema_lib 67 | File.read(File.dirname(__FILE__) + "/pea_std_lib/schema.js") 68 | end 69 | 70 | def symbols 71 | File.read(File.dirname(__FILE__) + "/pea_std_lib/symbols.js") 72 | end 73 | 74 | def css_preprocessor 75 | File.read(File.dirname(__FILE__) + "/pea_std_lib/css_preprocessor.js") 76 | end 77 | 78 | def indent! 79 | @indent += 2 80 | end 81 | 82 | def dedent! 83 | @indent -= 2 84 | end 85 | 86 | def padding(by = 0) 87 | assert { @indent + by >= 0 } 88 | " " * (@indent + by) 89 | end 90 | 91 | def eval_assignment_declarations 92 | names = find_assignments 93 | if names.any? 94 | vars = padding 95 | vars += "let " 96 | vars += names.map { |names| sub_q(names) }.join(", ") 97 | vars += ";" << "\n" 98 | end 99 | vars || "" 100 | end 101 | 102 | def find_assignments 103 | nodes = @ast.flat_map do |node| 104 | if node.is_a?(AST::If) 105 | node.pass + node.branches.flatten 106 | else 107 | [node] 108 | end 109 | end 110 | 111 | (nodes 112 | .filter { |node| node.is_a?(AST::Assign) } 113 | .uniq { |node| node.name } 114 | .select { |node| !@fn_arg_names.include?(node.name) } 115 | .map(&:name) + 116 | nodes 117 | .select(&:declare?) 118 | .flat_map { |node| node.captures } 119 | .uniq).uniq 120 | end 121 | 122 | def eval_expr(node) 123 | case node 124 | when AST::Class 125 | eval_class node 126 | when AST::SimpleAssignment, AST::SimpleReassignment 127 | eval_assignment node 128 | when AST::SimpleSchemaAssignment 129 | eval_simple_schema_assignment node 130 | when AST::SchemaUnion 131 | eval_schema_union node 132 | when AST::SchemaIntersect 133 | eval_schema_intersect node 134 | when AST::Assign 135 | eval_assignment node 136 | when AST::ArrayLiteral 137 | eval_array_literal node 138 | when AST::ObjectLiteral 139 | eval_object_literal node 140 | when AST::ArrowMethodObjectEntry 141 | eval_arrow_method_object_entry node 142 | when AST::SimpleObjectEntry 143 | eval_object_entry node 144 | when AST::FunctionObjectEntry 145 | eval_function_object_entry node 146 | when AST::SpreadObjectEntry 147 | eval_spread_object_entry node 148 | when AST::Bool 149 | eval_bool node 150 | when AST::Int 151 | eval_int node 152 | when AST::Float 153 | eval_float node 154 | when AST::SimpleString 155 | eval_simple_string node 156 | when AST::SingleLineDefWithArgs 157 | eval_single_line_fn_with_args node 158 | when AST::SingleLineDefWithoutArgs 159 | eval_single_line_fn_without_args node 160 | when AST::SingleLineArrowFnWithoutArgs 161 | eval_arrow_fn_without_args node 162 | when AST::SingleLineArrowFnWithArgs 163 | eval_single_line_arrow_fn_with_args node 164 | when AST::MultiLineArrowFnWithArgs 165 | eval_arrow_fn_with_args node 166 | when AST::SingleLineArrowFnWithOneArg 167 | eval_arrow_fn_with_one_arg node 168 | when AST::StaticMethod 169 | eval_static_method node 170 | when AST::MultilineDefWithoutArgs 171 | eval_multiline_def_without_args node 172 | when AST::MultilineDefWithArgs 173 | eval_multiline_def_with_args node 174 | when AST::ShortFnWithArgs 175 | eval_short_fn_with_args node 176 | when AST::ShortFn 177 | eval_short_fn node 178 | when AST::AnonIdLookup 179 | eval_anon_id_lookup 180 | when AST::Empty 181 | "" 182 | when AST::Return 183 | eval_return node 184 | when AST::FnCall 185 | eval_function_call node 186 | when AST::IdLookup 187 | eval_identifier_lookup node 188 | when AST::If 189 | eval_if_expression node 190 | when AST::Else 191 | eval_else node 192 | when AST::ElseIf 193 | eval_else_if node 194 | when AST::SchemaCapture 195 | eval_schema_capture node 196 | when AST::DotAssignment 197 | eval_dot_assignment node 198 | when AST::Dot 199 | eval_dot node 200 | when AST::Range 201 | eval_range node 202 | when AST::Op 203 | eval_operator node 204 | when AST::SimpleForOfLoop 205 | eval_simple_for_of_loop node 206 | when AST::ForOfObjDeconstructLoop 207 | eval_for_of_obj_descontruct_loop node 208 | when AST::SchemaDefinition 209 | eval_schema_definition node 210 | when AST::SchemaObjectLiteral 211 | eval_schema_object_literal node 212 | when AST::Await 213 | eval_await node 214 | when AST::ExprComponent 215 | eval_expr_component node 216 | when AST::ExprComponentWithAttributes 217 | eval_expr_component_with_attrs node 218 | when AST::BodyComponentWithoutAttrs 219 | eval_body_component_without_attrs node 220 | when AST::EscapedElementExpr 221 | eval_escaped_element_expr node 222 | when AST::This 223 | "this" 224 | when AST::New 225 | eval_new node 226 | when AST::ShortHandConstructor 227 | eval_short_hand_constructor node 228 | when AST::DynamicLookup 229 | eval_dynamic_lookup node 230 | when AST::OneLineGetter 231 | eval_one_line_getter node 232 | when AST::InstanceProperty 233 | eval_instance_property node 234 | when AST::CaseFunctionDefinition 235 | eval_case_function_definition node 236 | when AST::ThisSchemaArg 237 | eval_this_schema_arg node 238 | when AST::Bind 239 | eval_bind node 240 | when AST::OptionalChain 241 | eval_optional_chain node 242 | when AST::ArrayAssignment 243 | eval_array_assignment node 244 | when AST::SimpleForInLoop 245 | eval_simple_for_in_loop node 246 | when AST::Null 247 | eval_null node 248 | when AST::SpreadExpr 249 | eval_spread_expr node 250 | when AST::SingleLineBindFunctionDefinition 251 | eval_single_line_bind_function_definition node 252 | when AST::MultiLineBindFunctionDefinition 253 | eval_multi_line_bind_function_definition node 254 | when AST::ArrayComprehension 255 | eval_array_comprehension node 256 | when AST::DefaultAssignment 257 | eval_default_assignment node 258 | when AST::PlusAssignment 259 | eval_plus_assignment node 260 | when AST::DefaultConstructorArg 261 | eval_default_constructor_arg node 262 | when AST::SimpleConstructorArg 263 | eval_simple_constructor_arg node 264 | when AST::EmptyCaseExpr 265 | eval_empty_case_expr node 266 | when AST::SimpleWhen 267 | eval_simple_when node 268 | when AST::CaseElse 269 | eval_case_else node 270 | when AST::Not 271 | eval_not node 272 | when AST::SimpleFnArgs 273 | eval_simple_fn_args node 274 | when AST::SimpleArg 275 | eval_simple_arg node 276 | when AST::SpreadArg 277 | eval_spread_arg node 278 | when AST::SimpleSchemaArg 279 | eval_simple_schema_arg node 280 | when AST::OneLineEnum 281 | eval_one_line_enum node 282 | when AST::EnumExpr 283 | eval_enum_expr node 284 | else 285 | binding.pry 286 | puts "no case matched node_type: #{node.class}" 287 | assert_not_reached! 288 | end 289 | end 290 | 291 | def eval_enum_expr(node) 292 | "#{node.enum_name}.#{node.variant_name}" 293 | end 294 | 295 | def eval_one_line_enum(node) 296 | e = "const #{node.name} = {\n" 297 | for variant in node.variant_names 298 | e += padding(2) + "#{variant}: Symbol('#{variant}'),\n" 299 | end 300 | e + padding + "}" 301 | end 302 | 303 | def eval_arrow_method_object_entry(node) 304 | fn = node.value 305 | args = eval_expr fn.args 306 | f = "#{node.key_name}(#{args}) {\n" 307 | f += "#{padding(2)}return #{eval_expr fn.return_expr};\n" 308 | f += "#{padding}}," 309 | end 310 | 311 | def eval_simple_schema_arg(node) 312 | node.name 313 | end 314 | 315 | def eval_spread_arg(node) 316 | "...#{node.name}" 317 | end 318 | 319 | def eval_simple_arg(node) 320 | node.name 321 | end 322 | 323 | def eval_simple_fn_args(node) 324 | node.args.map { |arg| eval_expr arg }.join ", " 325 | end 326 | 327 | def eval_not(node) 328 | "!#{eval_expr node.expr}" 329 | end 330 | 331 | def eval_case_else(node) 332 | # else is put in by `eval_empty_case_expr` 333 | c = "{\n" 334 | c += Compiler.new(node.body, @indent + 2).eval + "\n" 335 | c += "#{padding}}" 336 | end 337 | 338 | def eval_simple_when(node) 339 | c = "if (#{eval_expr node.expr}) {\n" 340 | c += Compiler.new(node.body, @indent + 2).eval + "\n" 341 | c += "#{padding}}" 342 | end 343 | 344 | def eval_empty_case_expr(node) 345 | return "" if node.cases.size == 0 346 | c = "#{eval_expr node.cases.first}" 347 | for case_ in node.cases[1..] 348 | c += " else #{eval_expr case_}" 349 | end 350 | c 351 | end 352 | 353 | def eval_default_constructor_arg(node) 354 | "#{node.name} = #{eval_expr node.expr}" 355 | end 356 | 357 | def eval_simple_constructor_arg(node) 358 | node.name 359 | end 360 | 361 | def eval_plus_assignment(node) 362 | "#{eval_expr node.lhs} += #{eval_expr node.expr}" 363 | end 364 | 365 | def eval_default_assignment(node) 366 | "#{eval_expr node.lhs} ||= #{eval_expr node.expr}" 367 | end 368 | 369 | def eval_multi_line_bind_function_definition(node) 370 | # ugly 371 | args = node.args.args.map(&:name).join ", " 372 | fn = "#{fn_prefix}#{node.function_name}(#{args}) {\n" 373 | indent! 374 | fn += "#{padding}if (!(this instanceof #{node.object_name})) throw new MatchError('Expected `this` to be a `#{node.object_name}`');\n" 375 | fn += Compiler.new(node.body, @indent).eval + "\n" 376 | dedent! 377 | fn += "#{padding}}" 378 | end 379 | 380 | def eval_array_comprehension(node) 381 | arr = eval_expr node.array_expr 382 | if node.if_expr 383 | arr += ".filter(#{node.variable} => #{eval_expr node.if_expr})" 384 | end 385 | "Array.from(#{arr}, #{node.variable} => #{eval_expr node.expr})" 386 | end 387 | 388 | def eval_single_line_bind_function_definition(node) 389 | # ugly args.args 390 | args = node.args.args.map(&:name).join ", " 391 | fn = "#{fn_prefix}#{node.function_name}(#{args}) {\n" 392 | indent! 393 | fn += "#{padding}if (!(this instanceof #{node.object_name})) throw new MatchError('Expected `this` to be a `#{node.object_name}`');\n" 394 | fn += "#{padding}return #{eval_expr node.return_expr};\n" 395 | dedent! 396 | fn += "#{padding}}" 397 | end 398 | 399 | def eval_spread_expr(node) 400 | "...#{eval_expr node.expr}" 401 | end 402 | 403 | def eval_null(node) 404 | "null" 405 | end 406 | 407 | def eval_this_schema_arg(node) 408 | eval_expr node.schema 409 | end 410 | 411 | def eval_simple_for_in_loop(node) 412 | f = "for (let #{node.variable} in #{eval_expr node.object_expr}) {\n" 413 | f += Compiler.new(node.body, @indent + 2).eval + "\n" 414 | f += "}" 415 | end 416 | 417 | def eval_array_assignment(node) 418 | a = "let _temp = #{eval_expr node.expr};\n" 419 | a += "s.verify([#{node.variables.map { |v| "s('#{v}')" }.join ", "}], _temp, 'Array');\n" 420 | a + "[#{node.variables.join ", "}] = _temp" 421 | end 422 | 423 | def eval_optional_chain(node) 424 | "#{padding}#{eval_expr node.lhs}?.#{node.property}" 425 | end 426 | 427 | def eval_bind(node) 428 | args = node.args.map { |arg| eval_expr arg }.join ", " 429 | if args.size > 0 430 | args = ", #{args}" 431 | end 432 | "#{eval_expr node.function}.call(#{eval_expr node.lhs}#{args})" 433 | end 434 | 435 | def eval_case_function_definition(node) 436 | f = "#{padding}#{fn_prefix}#{node.name}(...args) {\n" 437 | # TODO: make this generic 438 | node.patterns.each_with_index do |s_case, i| 439 | if s_case.patterns.all? { |arg| arg.is_a? AST::SimpleSchemaArg } 440 | schemas = s_case.patterns.map(&:schema_name) 441 | args = s_case.patterns.map(&:name) 442 | elsif s_case.patterns.all? { |arg| arg.is_a? AST::SchemaInt } 443 | schemas = s_case.patterns.map(&:value) 444 | args = [] 445 | elsif s_case.patterns.all? { |arg| arg.is_a? AST::SimpleArg } 446 | schemas = s_case.patterns.map { |p| "s('#{p.name}')" } 447 | args = s_case.patterns.map(&:name) 448 | else 449 | assert_not_reached! 450 | end 451 | if i == 0 452 | f += "#{padding} if " 453 | else 454 | f += " else if " 455 | end 456 | f += "(" 457 | if s_case.this_pattern 458 | f += "s.check(#{eval_expr s_case.this_pattern}, this) && " 459 | end 460 | f += "s.check([#{schemas.join ", "}], args)) {\n" 461 | 462 | args.each_with_index do |arg, i| 463 | f += "#{padding} let #{arg} = args[#{i}];\n" 464 | end 465 | f += "#{padding}#{Compiler.new(s_case.body, @indent + 4).eval}\n" 466 | if i == node.patterns.size - 1 467 | f += "#{padding} }\n" 468 | else 469 | f += "#{padding} }" 470 | end 471 | end 472 | f.rstrip! 473 | f += "#{padding} else throw new MatchError();\n" 474 | f + "}" 475 | end 476 | 477 | def eval_instance_property(node) 478 | "#{padding}#{node.name} = #{eval_expr node.expr};" 479 | end 480 | 481 | def eval_one_line_getter(node) 482 | m = "#{padding}get #{node.name}() {\n" 483 | m += "#{padding} return #{eval_expr node.expr};\n" 484 | m += "#{padding}}" 485 | end 486 | 487 | def eval_dynamic_lookup(node) 488 | "#{eval_expr node.lhs}[#{eval_expr node.expr}]" 489 | end 490 | 491 | def eval_short_hand_constructor(node) 492 | args = node.args.map { |arg| eval_expr arg }.join ", " 493 | c = "#{padding}constructor(#{args}) {\n" 494 | for arg in node.args.map(&:name) 495 | c += "#{padding} this.#{arg} = #{arg};\n" 496 | end 497 | c += "#{padding}}" 498 | end 499 | 500 | def eval_new(node) 501 | args = node.args.map { |node| eval_expr node }.join ", " 502 | "new #{eval_expr node.class_expr}(#{args})" 503 | end 504 | 505 | def eval_static_method(node) 506 | "#{padding}static #{eval_multiline_def_with_args(node).lstrip}" 507 | end 508 | 509 | def eval_dot_assignment(node) 510 | "#{eval_expr node.lhs} = #{eval_expr node.expr}" 511 | end 512 | 513 | def eval_class(node) 514 | super_class = " extends #{node.parent_class}" if node.parent_class 515 | c = "class #{node.name}#{super_class} {\n" 516 | indent! 517 | @is_class_definition = true 518 | for entry in node.entries 519 | c += "#{eval_expr entry}\n" 520 | end 521 | @is_class_definition = false 522 | dedent! 523 | c += "}" 524 | end 525 | 526 | def eval_escaped_element_expr(node) 527 | node = node.expr 528 | value = if node.is_a?(AST::IdLookup) 529 | "this.#{node.value} || #{node.value}" 530 | else 531 | eval_expr node 532 | end 533 | "${#{value}}" 534 | end 535 | 536 | def kebab_case(name) 537 | name.gsub(/([a-z\d])([A-Z])/, '\1-\2').downcase 538 | end 539 | 540 | def create_shadow_root 541 | "this.attachShadow({ mode: 'open' });" 542 | end 543 | 544 | def define_component(node) 545 | "customElements.define('#{kebab_case node.name}', #{node.name})" 546 | end 547 | 548 | def eval_body_component_without_attrs(node) 549 | c = "class #{node.name} extends HTMLElement {\n" 550 | node.constructor_body 551 | .select { |node| node.is_a? AST::SimpleAssignment } 552 | .each do |node| 553 | c += " #{node.name} = #{eval_expr node.expr};\n" 554 | end 555 | c += " constructor() {\n" 556 | c += " super();\n" 557 | c += " #{create_shadow_root}\n" 558 | c += " }\n" 559 | c += " connectedCallback() {\n" 560 | c += " #{eval_render node};\n" 561 | c += " }\n" 562 | c += "}\n" 563 | c + define_component(node) 564 | end 565 | 566 | def get_attr_names(node) 567 | node.attributes.properties.map { |attr, _| attr } 568 | end 569 | 570 | def assign_attributes(node, padding) 571 | node.attributes.properties.map do |attr, schema| 572 | assert { schema.is_a? AST::SchemaCapture } 573 | "#{padding}this.#{attr} = this.getAttribute('#{attr}');" 574 | end.join "\n" 575 | end 576 | 577 | def get_var_queries(elem, path = elem.expr.name) 578 | assert { elem.expr.children.size == 1 } 579 | assert { elem.expr.children[0].is_a? AST::EscapedElementExpr } 580 | assert { elem.expr.children[0].value.is_a? AST::IdLookup } 581 | { 582 | elem.expr.children[0].value.value => { 583 | :path => path, 584 | :expr => elem.expr.children[0], 585 | }, 586 | } 587 | end 588 | 589 | def eval_expr_component_with_attrs(node) 590 | c = "class #{node.name} extends HTMLElement {\n" 591 | c += " static get observedAttributes() {\n" 592 | c += " return #{get_attr_names(node).to_s};\n" 593 | c += " }\n" 594 | c += " constructor() {\n" 595 | c += " super();\n" 596 | c += " #{create_shadow_root}\n" 597 | c += " }\n" 598 | c += " connectedCallback() {\n" 599 | c += "#{assign_attributes(node, " ")}\n" 600 | c += " #{eval_render node}\n" 601 | c += " }\n" 602 | c += " attributeChangedCallback(name, oldValue, newValue) {\n" 603 | c += " if (this.shadowRoot.innerHTML.trim() === '') return;\n" 604 | var_name_to_query = get_var_queries node 605 | var_name_to_query.each do |key, value| 606 | c += " if (name === '#{key}') {\n" 607 | c += " this.#{key} = newValue;\n" 608 | c += " this.shadowRoot.querySelector('#{value[:path]}').innerHTML = `#{eval_expr value[:expr]}`;\n" 609 | c += " }\n" 610 | end 611 | c += " }\n" 612 | c += "}\n" 613 | c + define_component(node) 614 | end 615 | 616 | def eval_render(node) 617 | "this.shadowRoot.innerHTML = #{eval_simple_element node.expr};" 618 | end 619 | 620 | def eval_simple_element(node) 621 | assert { node.is_a? AST::SimpleElement } 622 | e = "`<#{node.name}>" 623 | e += " #{node.children.map { |el| eval_expr el }.join("\n")}" 624 | e += "`" 625 | end 626 | 627 | def eval_expr_component(node) 628 | c = "class #{node.name} extends HTMLElement {\n" 629 | c += " constructor() {\n" 630 | c += " super();\n" 631 | c += " #{create_shadow_root}\n" 632 | c += " #{eval_render node};\n" 633 | c += " }\n" 634 | c += "}\n" 635 | c + define_component(node) 636 | end 637 | 638 | def eval_await(node) 639 | "await #{eval_expr node.expr}" 640 | end 641 | 642 | def eval_simple_schema_assignment(node) 643 | "#{node.name} = s.verify(#{node.schema_name}, #{eval_expr node.expr}, '#{node.schema_name}')" 644 | end 645 | 646 | def unpack_object(object_str) 647 | assert { object_str[0] == "{" } 648 | assert { object_str[-1] == "}" } 649 | object_str[1...-1].strip 650 | end 651 | 652 | def eval_schema_intersect(node) 653 | schema = "{ " 654 | schema += node.schema_exprs.map do |expr| 655 | case expr 656 | when AST::IdLookup 657 | "...#{eval_expr expr}" 658 | when AST::SchemaObjectLiteral 659 | unpack_object eval_expr(expr) 660 | else 661 | assert_not_reached! 662 | end 663 | end.join ", " 664 | schema + " }" 665 | end 666 | 667 | def eval_schema_union(node) 668 | "s.union(#{node.schema_exprs.map { |expr| eval_expr expr }.join ", "})" 669 | end 670 | 671 | def eval_schema_object_literal(node) 672 | schema_obj = "{ " 673 | for name, value in node.properties 674 | schema_obj += "#{name}: #{eval_expr value}, " 675 | end 676 | # remove last ", " 677 | schema_obj[0...-2] + " }" 678 | end 679 | 680 | def eval_schema_definition(node) 681 | "const #{node.name} = #{eval_expr node.schema_expr}" 682 | end 683 | 684 | def eval_for_of_obj_descontruct_loop(node) 685 | properties = node.iter_properties.join ", " 686 | for_loop = "for (let { #{properties} } of #{eval_expr node.arr_expr}) {\n" 687 | for_loop += Compiler.new(node.body, @indent + 2).eval + "\n" 688 | for_loop += "#{padding}}" 689 | end 690 | 691 | def eval_simple_for_of_loop(node) 692 | for_loop = "for (let #{node.iter_name} of #{eval_expr node.arr_expr}) {\n" 693 | for_loop += Compiler.new(node.body, @indent + 2).eval + "\n" 694 | for_loop += "#{padding}}" 695 | end 696 | 697 | def fn_prefix 698 | if @is_class_definition 699 | "" 700 | else 701 | "function " 702 | end 703 | end 704 | 705 | def eval_multiline_def_without_args(fn_node) 706 | fn = "#{padding}#{fn_prefix}#{fn_node.name}() {\n" 707 | fn += Compiler.new(fn_node.body, @indent + 2).eval + "\n" 708 | fn += "#{padding}}" 709 | fn 710 | end 711 | 712 | def eval_dot(node) 713 | "#{eval_expr(node.lhs)}.#{eval_expr(node.rhs)}" 714 | end 715 | 716 | def eval_range(node) 717 | "new Range(#{eval_expr node.lhs}, #{eval_expr node.rhs})" 718 | end 719 | 720 | def eval_dbl_eq(node) 721 | "#{eval_expr(node.lhs)}[Symbol.peacock_equals](#{eval_expr(node.rhs)})" 722 | end 723 | 724 | def eval_in(node) 725 | "#{eval_expr(node.rhs)}[Symbol.peacock_contains](#{eval_expr(node.lhs)})" 726 | end 727 | 728 | def eval_operator(node) 729 | return eval_in(node) if node.type == :in 730 | return eval_dbl_eq(node) if node.type == :"==" 731 | op = node.type 732 | op = "%" if node.type == :mod 733 | 734 | "#{eval_expr(node.lhs)} #{op} #{eval_expr(node.rhs)}" 735 | end 736 | 737 | def eval_schema_capture(node) 738 | "s('#{node.name}')" 739 | end 740 | 741 | def eval_else_if(node) 742 | b = " else if (#{eval_expr node.cond}) {\n" 743 | b += Compiler.new(node.body, @indent + 2).eval + "\n" 744 | b += "}" 745 | b 746 | end 747 | 748 | def eval_else(node) 749 | b = " else {\n" 750 | b += Compiler.new(node.body, @indent + 2).eval + "\n" 751 | b += "}" 752 | b 753 | end 754 | 755 | def eval_if_expression(node) 756 | cond = eval_expr(node.cond) 757 | pass_body = Compiler.new(node.pass, @indent + 2).eval_without_variable_declarations 758 | 759 | i = "if (#{cond}) {\n" 760 | i += "#{pass_body}\n" 761 | i += "#{padding}}" 762 | i += node.branches.map { |branch| eval_expr branch }.join "" 763 | 764 | i 765 | end 766 | 767 | def eval_simple_string(node) 768 | if node.value.include?("\n") 769 | "`#{node.value}`" 770 | else 771 | "\"#{node.value}\"" 772 | end 773 | end 774 | 775 | def eval_int(node) 776 | "#{node.value}" 777 | end 778 | 779 | def eval_float(node) 780 | "#{node.value}" 781 | end 782 | 783 | def eval_bool(node) 784 | "#{node.value}" 785 | end 786 | 787 | def eval_array_literal(node) 788 | elements = node.expr.map { |n| eval_expr n }.join(", ") 789 | "[#{elements}]" 790 | end 791 | 792 | def eval_object_entry(node) 793 | "#{padding}#{node.key_name}: #{eval_expr node.value}" 794 | end 795 | 796 | def eval_spread_object_entry(node) 797 | "#{padding}...#{eval_expr node.expr}" 798 | end 799 | 800 | def eval_function_object_entry(node) 801 | fn = node.value 802 | args = eval_expr fn.args 803 | output = "#{node.key_name}(#{args}) {\n" 804 | output += Compiler.new(fn.body, @indent + 2, fn_arg_names: fn.args.args.map(&:name)).eval + "\n" 805 | output + "#{padding}}" 806 | end 807 | 808 | def eval_object_literal(node) 809 | return "{}" if node.expr.size == 0 810 | has_function = node.expr.any? do |node| 811 | node.is_a?(AST::ArrowMethodObjectEntry) || node.is_a?(AST::FunctionObjectEntry) 812 | end 813 | whitespace = if node.expr.size > 3 || has_function 814 | indent! 815 | "\n" 816 | else 817 | " " 818 | end 819 | object_literal = "{#{whitespace}" 820 | object_literal += padding + node.expr.map { |entry| eval_expr entry }.join(",#{whitespace}") 821 | dedent! if whitespace != " " 822 | object_literal += "#{whitespace}#{padding}}" 823 | end 824 | 825 | def sub_q(sym) 826 | sym 827 | .sub("?", "_q") 828 | .sub("!", "_b") 829 | end 830 | 831 | def eval_declaration(node) 832 | "const #{sub_q(node.name)} = #{eval_expr(node.expr)}" 833 | end 834 | 835 | def eval_assignment(node) 836 | "#{sub_q(node.name)} = #{eval_expr(node.expr)}" 837 | end 838 | 839 | def eval_arrow_fn_with_one_arg(node) 840 | "(#{node.arg}) => #{eval_expr node.return_expr}" 841 | end 842 | 843 | def eval_multiline_def_with_args(fn_node) 844 | args = eval_expr fn_node.args 845 | 846 | fn = "#{padding}#{fn_prefix}#{fn_node.name}(#{args}) {\n" 847 | fn += "#{padding(2)}#{schema_arg_assignments fn_node.args}".rstrip 848 | fn += Compiler.new(fn_node.body, @indent + 2).eval + "\n" 849 | fn += "#{padding}}" 850 | fn 851 | end 852 | 853 | def schema_arg_assignments(args_node) 854 | assignments = args_node.args 855 | .select { |node| node.is_a?(AST::SimpleSchemaArg) } 856 | .map do |arg| 857 | "#{padding}#{arg.name} = s.verify(#{arg.schema_name}, #{arg.name});" 858 | end.join("\n") 859 | 860 | assignments += args_node.args 861 | .map.with_index { |node, i| { node: node, i: i } } 862 | .select { |h| h[:node].is_a?(AST::NullSchema) } 863 | .map { |h| "#{padding}s.verify(null, args[#{h[:i]}], 'null')" } 864 | .join("\n") 865 | 866 | if assignments.empty? 867 | "" 868 | else 869 | assignments + "\n" 870 | end 871 | end 872 | 873 | def eval_single_line_fn_without_args(fn_node) 874 | fn = "#{padding}#{fn_prefix}#{fn_node.name}() {\n" 875 | indent! 876 | fn += "#{padding}return #{eval_expr fn_node.return_value};\n" 877 | dedent! 878 | fn += "#{padding}}" 879 | fn 880 | end 881 | 882 | def eval_single_line_fn_with_args(fn_node) 883 | args = if fn_node.args.args.any?(&:schema?) 884 | "...args" 885 | else 886 | eval_expr fn_node.args 887 | end 888 | 889 | fn = "#{padding}#{fn_prefix}#{fn_node.name}(#{args}) {\n" 890 | indent! 891 | fn += schema_arg_assignments(fn_node.args) 892 | fn += "#{padding}return #{eval_expr fn_node.return_value};\n" 893 | dedent! 894 | fn += "#{padding}}" 895 | fn 896 | end 897 | 898 | def eval_single_line_arrow_fn_with_args(node) 899 | args = eval_expr node.args 900 | 901 | fn = "(#{args}) => {\n" 902 | indent! 903 | fn += schema_arg_assignments(node.args) 904 | fn += padding + "return #{eval_expr node.return_expr};\n" 905 | dedent! 906 | fn + "#{padding}}" 907 | end 908 | 909 | def eval_arrow_fn_with_args(node) 910 | args = eval_expr node.args 911 | 912 | fn = "(#{args}) => {\n" 913 | fn += schema_arg_assignments(node.args) 914 | fn += Compiler.new(node.body, @indent + 2, fn_arg_names: node.args.value.map(&:name)).eval + "\n" 915 | fn + "}" 916 | end 917 | 918 | def eval_arrow_fn_without_args(node) 919 | "(() => { return #{eval_expr node.return_expr}; })" 920 | end 921 | 922 | def eval_short_fn_with_args(node) 923 | "(#{node.args.join ", "} => #{eval_expr node.return_expr})" 924 | end 925 | 926 | def eval_anon_id_lookup 927 | "_it" 928 | end 929 | 930 | def eval_short_fn(node) 931 | "(#{eval_anon_id_lookup} => #{eval_expr node.return_expr})" 932 | end 933 | 934 | def eval_return(node) 935 | "return #{eval_expr node.expr}" 936 | end 937 | 938 | def eval_function_call(node) 939 | args = node.args.map { |arg| eval_expr arg }.join ", " 940 | fn = eval_expr node.expr 941 | "#{fn}(#{args})" 942 | end 943 | 944 | def eval_identifier_lookup(node) 945 | assert { node.value } 946 | sub_q(node.value) 947 | end 948 | end 949 | -------------------------------------------------------------------------------- /lib/parser.rb: -------------------------------------------------------------------------------- 1 | require "utils" 2 | require "ast" 3 | require "lexer" 4 | 5 | class ParserError 6 | attr_reader :msg, :start_pos, :end_pos 7 | 8 | def initialize(msg, start_pos, end_pos) 9 | @msg = msg 10 | @start_pos 11 | @end_pos = end_pos 12 | end 13 | end 14 | 15 | class SimpleAssignmentError < ParserError 16 | end 17 | 18 | class Parser 19 | attr_reader :tokens, :program_string, :pos, :errors 20 | 21 | def self.can_parse?(_self) 22 | not_implemented! 23 | end 24 | 25 | def self.from(_self) 26 | self.new(_self.tokens, _self.program_string, _self.pos, _self.errors) 27 | end 28 | 29 | def initialize(tokens, program_string, pos = 0, errors = []) 30 | @tokens = tokens 31 | @program_string = program_string 32 | @pos = pos 33 | @errors = errors 34 | end 35 | 36 | def consume_parser!(parser_klass, *parse_args, **parse_opts) 37 | parser = parser_klass.from(self) 38 | expr_n = parser.parse! *parse_args, **parse_opts 39 | @pos = parser.pos 40 | expr_n 41 | end 42 | 43 | def current_token 44 | @tokens[@pos] 45 | end 46 | 47 | def prev_token 48 | @tokens[@pos - 1] 49 | end 50 | 51 | def peek_token 52 | @tokens[@pos + 1] 53 | end 54 | 55 | def peek_token_twice 56 | @tokens[@pos + 2] 57 | end 58 | 59 | def peek_token_thrice 60 | @tokens[@pos + 3] 61 | end 62 | 63 | def rest_of_line 64 | rest_of_program = @program_string[current_token.start_pos..] 65 | rest_of_program[0..rest_of_program.index("\n")] 66 | end 67 | 68 | def current_line 69 | @program_string[0..current_token&.start_pos] 70 | .split("\n") 71 | .count 72 | end 73 | 74 | def new_line?(offset = 0) 75 | return true if !prev_token || !current_token 76 | prev = @tokens[@pos + offset - 1] 77 | curr = @tokens[@pos + offset] 78 | @program_string[prev.start_pos..curr.start_pos].include? "\n" 79 | end 80 | 81 | def parser_not_implemented!(parser_klasses) 82 | puts "Not Implemented, only supporting the following parsers - " 83 | pp parser_klasses 84 | not_implemented! 85 | end 86 | 87 | def consume_first_valid_parser!(parser_klasses, &catch_block) 88 | parser_klass = parser_klasses.find { |klass| klass.can_parse? self } 89 | if !parser_klass 90 | if catch_block 91 | catch_block.call 92 | else 93 | parser_not_implemented! parser_klasses 94 | end 95 | end 96 | consume_parser! parser_klass 97 | end 98 | 99 | def consume!(token_type) 100 | assert { token_type == current_token.type } 101 | @pos += 1 102 | return prev_token 103 | end 104 | 105 | def consume_any! 106 | @pos += 1 107 | return prev_token 108 | end 109 | 110 | def consume_if_present!(token_type) 111 | consume! token_type if current_token.type == token_type 112 | end 113 | 114 | def parse! 115 | ProgramParser.from(self).parse! 116 | end 117 | end 118 | 119 | ### FUNCTION ARGS START 120 | 121 | class SimpleSchemaArgParser < Parser 122 | def self.can_parse?(_self) 123 | _self.current_token&.type == :identifier && 124 | _self.peek_token.type == :open_paren && 125 | _self.peek_token_twice.type == :identifier && 126 | _self.peek_token_thrice.type == :close_paren 127 | end 128 | 129 | def parse! 130 | args = [] 131 | schema_name_t = consume! :identifier 132 | consume! :open_paren 133 | var_t = consume! :identifier 134 | close_t = consume! :close_paren 135 | AST::SimpleSchemaArg.new( 136 | schema_name_t.value, 137 | var_t.value, 138 | schema_name_t.start_pos, 139 | close_t.end_pos 140 | ) 141 | end 142 | end 143 | 144 | # broken :/ 145 | class SchemaIntParser < Parser 146 | def self.can_parse?(_self) 147 | _self.current_token.type == :int_lit 148 | end 149 | 150 | def parse! 151 | int_t = consume! :int_lit 152 | AST::SchemaInt.new(int_t) 153 | end 154 | end 155 | 156 | class SimpleArgParser < Parser 157 | def self.can_parse?(_self) 158 | _self.current_token&.type == :identifier && 159 | _self.peek_token.is_one_of?(:comma, :close_paren) 160 | end 161 | 162 | def parse! 163 | name_t = consume! :identifier 164 | AST::SimpleArg.new(name_t.value, name_t.start_pos, name_t.end_pos) 165 | end 166 | end 167 | 168 | class NullSchemaParser < Parser 169 | def self.can_parse?(_self) 170 | _self.current_token&.type == :null 171 | end 172 | 173 | def parse! 174 | null_t = consume! :null 175 | AST::NullSchema.new(null_t.start_pos, null_t.end_pos) 176 | end 177 | end 178 | 179 | class SchemaArgParser < Parser 180 | PARSERS = [ 181 | SimpleSchemaArgParser, 182 | NullSchemaParser, 183 | SchemaIntParser, 184 | SimpleArgParser, 185 | ] 186 | 187 | def self.can_parse?(_self) 188 | PARSERS.any? { |klass| klass.can_parse? _self } 189 | end 190 | 191 | def parse! 192 | consume_first_valid_parser! PARSERS 193 | end 194 | end 195 | 196 | class SpreadArgParser < Parser 197 | def self.can_parse?(_self) 198 | _self.current_token.type == :"..." 199 | end 200 | 201 | def parse! 202 | spread_t = consume! :"..." 203 | name_t = consume! :identifier 204 | AST::SpreadArg.new(name_t.value, spread_t.start_pos, name_t.end_pos) 205 | end 206 | end 207 | 208 | class SimpleFnArgsParser < Parser 209 | ARG_PARSERS = [ 210 | SimpleArgParser, 211 | SpreadArgParser, 212 | SchemaArgParser, 213 | ] 214 | 215 | def parse! 216 | open_t = consume! :open_paren 217 | 218 | if current_token.type == :close_paren 219 | close_t = consume! :close_paren 220 | return AST::SimpleFnArgs.new([], open_t.start_pos, close_t.end_pos) 221 | end 222 | 223 | args = [] 224 | 225 | loop do 226 | args.push consume_first_valid_parser! ARG_PARSERS 227 | break if current_token.type == :close_paren 228 | consume! :comma 229 | end 230 | close_t = consume! :close_paren 231 | 232 | AST::SimpleFnArgs.new(args, open_t.start_pos, close_t.end_pos) 233 | end 234 | end 235 | 236 | ### FUNCTION ARGS END 237 | 238 | ### FUNCTION DEFINITION START 239 | 240 | class SingleLineDefWithArgsParser < Parser 241 | def self.can_parse?(_self) 242 | _self.current_token&.type == :function && 243 | _self.peek_token.type == :identifier && 244 | _self.peek_token_twice.type == :open_paren && 245 | _self.rest_of_line.include?("=") 246 | end 247 | 248 | def parse! 249 | function_t = consume! :function 250 | fn_name_t = consume! :identifier 251 | args_n = consume_parser! SimpleFnArgsParser 252 | consume! :"=" 253 | return_value_n = consume_parser! ExprParser 254 | 255 | AST::SingleLineDefWithArgs.new( 256 | fn_name_t.value, 257 | args_n, 258 | return_value_n, 259 | function_t.start_pos, 260 | return_value_n.end_pos 261 | ) 262 | end 263 | end 264 | 265 | class SingleLineDefWithoutArgsParser < Parser 266 | def self.can_parse?(_self) 267 | _self.current_token&.type == :function && 268 | _self.peek_token.type == :identifier && 269 | _self.rest_of_line.include?("=") 270 | end 271 | 272 | def parse! 273 | function_t = consume! :function 274 | fn_name_t = consume! :identifier 275 | consume! :"=" 276 | return_value_n = consume_parser! ExprParser 277 | 278 | AST::SingleLineDefWithoutArgs.new( 279 | fn_name_t.value, 280 | return_value_n, 281 | function_t.start_pos, 282 | return_value_n.end_pos 283 | ) 284 | end 285 | end 286 | 287 | class MultilineDefWithoutArgsParser < Parser 288 | def self.can_parse?(_self) 289 | _self.current_token&.type == :function && 290 | _self.peek_token.type == :identifier 291 | end 292 | 293 | def parse! 294 | function_t = consume! :function 295 | fn_name_t = consume! :identifier 296 | body = consume_parser! FunctionBodyParser 297 | end_t = consume! :end 298 | AST::MultilineDefWithoutArgs.new( 299 | fn_name_t.value, 300 | body, 301 | function_t.start_pos, 302 | end_t.end_pos 303 | ) 304 | end 305 | end 306 | 307 | class MultilineDefWithArgsParser < Parser 308 | def self.can_parse?(_self) 309 | _self.current_token&.type == :function && 310 | _self.peek_token.type == :identifier && 311 | _self.peek_token_twice.type == :open_paren 312 | end 313 | 314 | def parse! 315 | function_t = consume! :function 316 | fn_name_t = consume! :identifier 317 | args_n = consume_parser! SimpleFnArgsParser 318 | body = consume_parser! FunctionBodyParser 319 | end_t = consume! :end 320 | AST::MultilineDefWithArgs.new( 321 | fn_name_t.value, 322 | args_n, 323 | body, 324 | function_t.start_pos, 325 | end_t.end_pos 326 | ) 327 | end 328 | end 329 | 330 | class ShortAnonFnWithNamedArgParser < Parser 331 | def self.can_parse?(_self) 332 | _self.current_token&.type == :"#\{" && 333 | _self.peek_token&.type == :"|" 334 | end 335 | 336 | def parse_args! 337 | args = [] 338 | 339 | consume! :"|" 340 | loop do 341 | args.push consume!(:identifier).value 342 | break if current_token.type == :"|" 343 | consume! :comma 344 | end 345 | consume! :"|" 346 | 347 | args 348 | end 349 | 350 | def parse! 351 | open_anon_t = consume! :"#\{" 352 | args = parse_args! 353 | return_expr_n = consume_parser! ExprParser 354 | return_expr_n ||= AST::Empty.new 355 | end_t = consume! :"}" 356 | AST::ShortFnWithArgs.new(args, return_expr_n, open_anon_t.start_pos, end_t.end_pos) 357 | end 358 | end 359 | 360 | class ShortAnonFnParser < Parser 361 | def self.can_parse?(_self) 362 | _self.current_token&.type == :"#\{" 363 | end 364 | 365 | def parse! 366 | open_anon_t = consume! :"#\{" 367 | return_expr_n = consume_parser! ExprParser 368 | return_expr_n ||= AST::Empty.new 369 | end_t = consume! :"}" 370 | AST::ShortFn.new(return_expr_n, open_anon_t.start_pos, end_t.end_pos) 371 | end 372 | end 373 | 374 | class AnonIdLookupParser < Parser 375 | def self.can_parse?(_self) 376 | _self.current_token&.type == :% 377 | end 378 | 379 | def parse! 380 | id_t = consume! :% 381 | AST::AnonIdLookup.new(id_t.start_pos, id_t.end_pos) 382 | end 383 | end 384 | 385 | class SingleLineArrowFnWithoutArgsParser < Parser 386 | def self.can_parse?(_self) 387 | _self.current_token&.type == :open_paren && 388 | _self.peek_token&.type == :close_paren && 389 | _self.peek_token_twice&.type == :"=>" 390 | end 391 | 392 | def parse! 393 | open_p_t = consume! :open_paren 394 | consume! :close_paren 395 | consume! :"=>" 396 | return_expr_n = consume_parser! ExprParser 397 | AST::SingleLineArrowFnWithoutArgs.new( 398 | return_expr_n, 399 | open_p_t.start_pos, 400 | return_expr_n.end_pos 401 | ) 402 | end 403 | end 404 | 405 | class MultiLineArrowFnWithArgsParser < Parser 406 | def self.can_parse?(_self) 407 | _self.current_token&.type == :open_paren && 408 | _self.rest_of_line&.include?("=>") && 409 | _self.rest_of_line&.include?("{") 410 | end 411 | 412 | def parse! 413 | args_n = consume_parser! SimpleFnArgsParser 414 | consume! :"=>" 415 | consume! :"{" 416 | body_n = consume_parser! FunctionBodyParser 417 | end_t = consume! :"}" 418 | AST::MultiLineArrowFnWithArgs.new(args_n, body_n, args_n.start_pos, end_t.end_pos) 419 | end 420 | end 421 | 422 | class SingleLineArrowFnWithArgsParser < Parser 423 | def self.can_parse?(_self) 424 | _self.current_token&.type == :open_paren && 425 | _self.rest_of_line&.include?("=>") 426 | end 427 | 428 | def parse! 429 | args_n = consume_parser! SimpleFnArgsParser 430 | consume! :"=>" 431 | return_expr_n = consume_parser! ExprParser 432 | AST::SingleLineArrowFnWithArgs.new(args_n, return_expr_n, args_n.start_pos, return_expr_n.end_pos) 433 | end 434 | end 435 | 436 | class SingleLineArrowFnWithOneArgParser < Parser 437 | def self.can_parse?(_self) 438 | _self.current_token&.type == :identifier && 439 | _self.peek_token&.type == :"=>" 440 | end 441 | 442 | def parse! 443 | arg_t = consume! :identifier 444 | consume! :"=>" 445 | return_expr_n = consume_parser! ExprParser 446 | AST::SingleLineArrowFnWithOneArg.new(arg_t.value, return_expr_n, arg_t.start_pos, return_expr_n.end_pos) 447 | end 448 | end 449 | 450 | class ReturnParser < Parser 451 | def self.can_parse?(_self) 452 | _self.current_token&.type == :return 453 | end 454 | 455 | def parse! 456 | return_t = consume! :return 457 | expr_n = consume_parser! ExprParser 458 | AST::Return.new(expr_n, return_t.start_pos, expr_n.end_pos) 459 | end 460 | end 461 | 462 | ### FUNCTION DEFINITION END 463 | 464 | ### FUNCTION CALLS START 465 | 466 | class FunctionCallWithoutArgs < Parser 467 | def self.can_parse?(_self, lhs) 468 | _self.current_token&.type == :open_paren && 469 | _self.peek_token.type == :close_paren 470 | end 471 | 472 | def parse!(lhs_n) 473 | open_p_t = consume! :open_paren 474 | close_p_t = consume! :close_paren 475 | 476 | AST::FnCall.new([], lhs_n, open_p_t.start_pos, close_p_t.end_pos) 477 | end 478 | end 479 | 480 | class FunctionCallWithArgs < Parser 481 | def self.can_parse?(_self, lhs) 482 | _self.current_token&.type == :open_paren 483 | end 484 | 485 | def parse!(lhs_n) 486 | open_p_t = consume! :open_paren 487 | args = [] 488 | loop do 489 | args.push consume_parser! ExprParser 490 | consume_if_present! :comma 491 | break if current_token.type == :close_paren 492 | end 493 | close_p_t = consume! :close_paren 494 | 495 | AST::FnCall.new(args, lhs_n, open_p_t.start_pos, close_p_t.end_pos) 496 | end 497 | end 498 | 499 | class FunctionCallWithArgsWithoutParens < Parser 500 | def self.can_parse?(_self, lhs) 501 | _self.current_token&.is_not_one_of?(:assign, :comma, :"]", :for, :if, :close_paren, :"}") && 502 | !_self.new_line? 503 | end 504 | 505 | def end_of_expr? 506 | current_token&.is_one_of? :"}", :close_paren 507 | end 508 | 509 | def parse!(lhs_n) 510 | args = [] 511 | 512 | loop do 513 | args.push consume_parser! ExprParser 514 | break if new_line? || end_of_expr? 515 | consume! :comma 516 | end 517 | 518 | AST::FnCall.new(args, lhs_n, lhs_n.end_pos, args.last.end_pos) 519 | end 520 | end 521 | 522 | ### FUNCTION CALLS END 523 | 524 | ### OPERATORS START 525 | 526 | class NotParser < Parser 527 | def self.can_parse?(_self) 528 | _self.current_token&.type == :not 529 | end 530 | 531 | def parse! 532 | not_t = consume! :not 533 | expr_n = consume_parser! ExprParser 534 | AST::Not.new(expr_n, not_t.start_pos, expr_n.end_pos) 535 | end 536 | end 537 | 538 | OPERATORS = [:+, :-, :*, :/, :"&&", :"||", :"===", :"!==", :>, :<, :">=", :"<=", :mod, :"==", :in] 539 | 540 | class OperatorParser < Parser 541 | def self.can_parse?(_self, lhs_n) 542 | _self.current_token&.is_one_of?(*OPERATORS) && 543 | !_self.new_line? 544 | end 545 | 546 | def parse!(lhs_n) 547 | operator_t = consume_any! 548 | rhs_n = consume_parser! ExprParser 549 | AST::Op.new(lhs_n, operator_t.type, rhs_n, lhs_n.start_pos, rhs_n.end_pos) 550 | end 551 | end 552 | 553 | class DotParser < Parser 554 | def self.can_parse?(_self, lhs) 555 | _self.current_token&.type == :dot 556 | end 557 | 558 | def parse!(lhs_n) 559 | dot_t = consume! :dot 560 | rhs_n = consume_parser! IdentifierLookupParser 561 | AST::Dot.new(lhs_n, ".", rhs_n, dot_t.start_pos, rhs_n.end_pos) 562 | end 563 | end 564 | 565 | class BindParser < Parser 566 | def self.can_parse?(_self, lhs) 567 | _self.current_token&.type == :"::" 568 | end 569 | 570 | def parse_args_without_parens! 571 | args = [] 572 | 573 | start_line = current_line 574 | loop do 575 | break if current_token.nil? 576 | break if current_line != start_line 577 | args.push consume_parser! ExprParser 578 | break if current_token.nil? 579 | break if current_line != start_line 580 | consume! :comma 581 | end 582 | 583 | args 584 | end 585 | 586 | def parse_args! 587 | return [] if new_line? 588 | return parse_args_without_parens! if current_token.type != :open_paren 589 | 590 | consume! :open_paren 591 | args = [] 592 | loop do 593 | args.push consume_parser! ExprParser 594 | break if current_token.type == :close_paren 595 | consume! :comma 596 | end 597 | consume! :close_paren 598 | 599 | args 600 | end 601 | 602 | def parse!(lhs_n) 603 | bind_t = consume! :"::" 604 | fn_name_n = consume_parser! IdentifierLookupParser 605 | args = parse_args! 606 | AST::Bind.new(lhs_n, fn_name_n, args) 607 | end 608 | end 609 | 610 | class OptionalChainParser < Parser 611 | def self.can_parse?(_self, lhs) 612 | _self.current_token&.type == :"?." 613 | end 614 | 615 | def parse!(lhs) 616 | q_t = consume! :"?." 617 | property_t = consume! :identifier 618 | AST::OptionalChain.new( 619 | lhs, 620 | property_t.value, 621 | q_t.start_pos, 622 | property_t.end_pos 623 | ) 624 | end 625 | end 626 | 627 | class AwaitParser < Parser 628 | def self.can_parse?(_self) 629 | _self.current_token&.type == :await 630 | end 631 | 632 | def parse! 633 | await_t = consume! :await 634 | expr_n = consume_parser! ExprParser 635 | AST::Await.new(expr_n, await_t.start_pos, expr_n.end_pos) 636 | end 637 | end 638 | 639 | ### OPERATORS END 640 | 641 | ### OBJECT LITERALS START 642 | 643 | class SimpleObjectEntryParser < Parser 644 | def self.can_parse?(_self) 645 | _self.current_token&.type == :identifier && 646 | _self.peek_token.type == :colon 647 | end 648 | 649 | def parse! 650 | key_t = consume! :identifier 651 | consume! :colon 652 | value_n = consume_parser! ExprParser 653 | 654 | AST::SimpleObjectEntry.new(key_t.value, value_n, key_t.start_pos, value_n.end_pos) 655 | end 656 | end 657 | 658 | class ArrowMethodObjectEntryParser < Parser 659 | def self.can_parse?(_self) 660 | _self.current_token&.type == :identifier && 661 | _self.peek_token.type == :open_paren && 662 | _self.rest_of_line.include?("=>") 663 | end 664 | 665 | def parse! 666 | key_t = consume! :identifier 667 | args_n = consume_parser! SimpleFnArgsParser 668 | consume! :"=>" 669 | return_expr_n = consume_parser! ExprParser 670 | AST::ArrowMethodObjectEntry.new( 671 | key_t.value, 672 | AST::SingleLineArrowFnWithArgs.new( 673 | args_n, 674 | return_expr_n, 675 | args_n.start_pos, 676 | return_expr_n.end_pos 677 | ), 678 | key_t.start_pos, 679 | return_expr_n.end_pos 680 | ) 681 | end 682 | end 683 | 684 | class FunctionObjectEntryParser < Parser 685 | def self.can_parse?(_self) 686 | _self.current_token&.type == :function 687 | end 688 | 689 | def parse! 690 | fn_n = consume_parser! FunctionDefinitionParser 691 | AST::FunctionObjectEntry.new(fn_n.name, fn_n, fn_n.start_pos, fn_n.end_pos) 692 | end 693 | end 694 | 695 | class IdentifierLookupParser < Parser 696 | def self.can_parse?(_self) 697 | _self.current_token&.type == :identifier 698 | end 699 | 700 | def parse! 701 | id_t = consume! :identifier 702 | AST::IdLookup.new(id_t.value, id_t.start_pos, id_t.end_pos) 703 | end 704 | end 705 | 706 | class SpreadObjectEntryParser < Parser 707 | def self.can_parse?(_self) 708 | _self.current_token&.type == :"..." 709 | end 710 | 711 | def parsers 712 | [ 713 | IdentifierLookupParser, 714 | ObjectParser, 715 | ] 716 | end 717 | 718 | def parse! 719 | spread_t = consume! :"..." 720 | expr_n = consume_first_valid_parser! parsers 721 | AST::SpreadObjectEntry.new(expr_n, spread_t.start_pos, spread_t.end_pos) 722 | end 723 | end 724 | 725 | class ObjectParser < Parser 726 | def self.can_parse?(_self) 727 | _self.current_token&.type == :"{" 728 | end 729 | 730 | ENTRY_PARSERS = [ 731 | SimpleObjectEntryParser, 732 | ArrowMethodObjectEntryParser, 733 | FunctionObjectEntryParser, 734 | SpreadObjectEntryParser, 735 | ] 736 | 737 | def parse! 738 | open_brace_t = consume! :"{" 739 | values = [] 740 | loop do 741 | break if current_token.type == :"}" 742 | values.push consume_first_valid_parser! ENTRY_PARSERS 743 | consume_if_present! :comma 744 | break if current_token.type == :"}" 745 | end 746 | close_brace_t = consume! :"}" 747 | AST::ObjectLiteral.new(values, open_brace_t.start_pos, close_brace_t.end_pos) 748 | end 749 | end 750 | 751 | ### OBJECT LITERALS END 752 | 753 | ### ARRAY LITERALS START 754 | 755 | class ArrayComprehensionParser < Parser 756 | def parse!(expr_n, start_pos) 757 | consume! :for 758 | id_t = consume! :identifier 759 | consume! :in 760 | array_expr_n = consume_parser! ExprParser 761 | if_expr_n = nil 762 | if current_token.type == :if 763 | consume! :if 764 | if_expr_n = consume_parser! ExprParser 765 | end 766 | close_sq_b_t = consume! :"]" 767 | AST::ArrayComprehension.new( 768 | expr_n, 769 | id_t.value, 770 | array_expr_n, 771 | if_expr_n, 772 | start_pos, 773 | close_sq_b_t.end_pos 774 | ) 775 | end 776 | end 777 | 778 | class ArrayParser < Parser 779 | def self.can_parse?(_self) 780 | _self.current_token&.type == :"[" 781 | end 782 | 783 | def parse! 784 | open_sq_b_t = consume! :"[" 785 | elems = [] 786 | if current_token.type == :"]" 787 | close_sq_b_t = consume! :"]" 788 | return AST::ArrayLiteral.new(elems, open_sq_b_t.start_pos, close_sq_b_t.end_pos) 789 | end 790 | 791 | first_expr_n = consume_parser! ExprParser 792 | 793 | if current_token.type == :for 794 | return consume_parser! ArrayComprehensionParser, first_expr_n, open_sq_b_t.start_pos 795 | end 796 | elems.push first_expr_n 797 | loop do 798 | break if current_token.type == :"]" 799 | consume_if_present! :comma 800 | elems.push consume_parser! ExprParser 801 | end 802 | close_sq_b_t = consume! :"]" 803 | AST::ArrayLiteral.new(elems, open_sq_b_t.start_pos, close_sq_b_t.end_pos) 804 | end 805 | end 806 | 807 | ### ARRAY LITERALS END 808 | 809 | ### PRIMATIVES START 810 | 811 | class IntParser < Parser 812 | def self.can_parse?(_self) 813 | _self.current_token&.type == :int_lit 814 | end 815 | 816 | def parse! 817 | int_t = consume! :int_lit 818 | AST::Int.new(int_t.value, int_t.start_pos, int_t.end_pos) 819 | end 820 | end 821 | 822 | class FloatParser < Parser 823 | def self.can_parse?(_self) 824 | _self.current_token&.type == :float_lit 825 | end 826 | 827 | def parse! 828 | float_t = consume! :float_lit 829 | AST::Float.new(float_t.value, float_t.start_pos, float_t.end_pos) 830 | end 831 | end 832 | 833 | class SimpleStringParser < Parser 834 | def self.can_parse?(_self) 835 | _self.current_token&.type == :str_lit 836 | end 837 | 838 | def parse! 839 | str_t = consume! :str_lit 840 | AST::SimpleString.new(str_t.value, str_t.start_pos, str_t.end_pos) 841 | end 842 | end 843 | 844 | class BoolParser < Parser 845 | def self.can_parse?(_self) 846 | _self.current_token&.type == :bool_lit 847 | end 848 | 849 | def parse! 850 | bool_t = consume! :bool_lit 851 | AST::Bool.new(bool_t.value, bool_t.start_pos, bool_t.end_pos) 852 | end 853 | end 854 | 855 | class NullParser < Parser 856 | def self.can_parse?(_self) 857 | _self.current_token&.type == :null 858 | end 859 | 860 | def parse! 861 | null_t = consume! :null 862 | AST::Null.new(null_t.start_pos, null_t.end_pos) 863 | end 864 | end 865 | 866 | ### PRIMATIVES END 867 | 868 | ### ASSIGNMENTS START 869 | 870 | class SimpleReassignmentParser < Parser 871 | def self.can_parse?(_self) 872 | _self.current_token&.type == :identifier && 873 | _self.peek_token&.type == :"=" 874 | end 875 | 876 | def parse! 877 | id_t = consume! :identifier 878 | consume! :"=" 879 | expr_n = consume_parser! ExprParser 880 | 881 | AST::SimpleReassignment.new(id_t.value, expr_n, id_t.start_pos, expr_n.end_pos) 882 | end 883 | end 884 | 885 | class SimpleAssignmentParser < Parser 886 | def self.can_parse?(_self) 887 | _self.current_token&.type == :identifier && 888 | _self.peek_token&.type == :assign 889 | end 890 | 891 | def parse! 892 | id_t = consume! :identifier 893 | consume! :assign 894 | expr_n = consume_parser! ExprParser 895 | 896 | AST::SimpleAssignment.new(id_t.value, expr_n, id_t.start_pos, expr_n.end_pos) 897 | end 898 | end 899 | 900 | class DefaultAssignmentParser < Parser 901 | def self.can_parse?(_self, lhs) 902 | _self.current_token&.type == :"||=" 903 | end 904 | 905 | def parse!(lhs_n) 906 | consume! :"||=" 907 | expr_n = consume_parser! ExprParser 908 | AST::DefaultAssignment.new(lhs_n, expr_n, lhs_n.start_pos, expr_n.end_pos) 909 | end 910 | end 911 | 912 | class PlusAssignmentParser < Parser 913 | def self.can_parse?(_self, lhs) 914 | _self.current_token&.type == :"+=" 915 | end 916 | 917 | def parse!(lhs_n) 918 | consume! :"+=" 919 | expr_n = consume_parser! ExprParser 920 | AST::PlusAssignment.new(lhs_n, expr_n, lhs_n.start_pos, expr_n.end_pos) 921 | end 922 | end 923 | 924 | class DotAssignmentParser < Parser 925 | def self.can_parse?(_self, lhs) 926 | lhs.is_a?(AST::Dot) && 927 | _self.current_token&.type == :assign 928 | end 929 | 930 | def parse!(lhs_n) 931 | assign_t = consume! :assign 932 | expr_n = consume_parser! ExprParser 933 | AST::DotAssignment.new( 934 | lhs_n, 935 | expr_n, 936 | assign_t.start_pos, 937 | expr_n.end_pos 938 | ) 939 | end 940 | end 941 | 942 | class ArrayAssignmentParser < Parser 943 | def self.can_parse?(_self) 944 | _self.current_token.type == :"[" && 945 | _self.rest_of_line.include?(":=") 946 | end 947 | 948 | def parse! 949 | open_t = consume! :"[" 950 | variables = [] 951 | loop do 952 | variables.push consume!(:identifier).value 953 | break if current_token.type == :"]" 954 | consume! :comma 955 | end 956 | close_t = consume! :"]" 957 | consume! :assign 958 | expr_n = consume_parser! ExprParser 959 | AST::ArrayAssignment.new( 960 | variables, 961 | expr_n, 962 | open_t.start_pos, 963 | expr_n.end_pos 964 | ) 965 | end 966 | end 967 | 968 | ### ASSIGNMENTS END 969 | 970 | ### CASE EXPRESSIONS START 971 | 972 | class SimpleWhenParser < Parser 973 | def self.can_parse?(_self) 974 | _self.current_token&.type == :when 975 | end 976 | 977 | def parse! 978 | when_t = consume! :when 979 | expr_n = consume_parser! ExprParser 980 | body = consume_parser! ProgramParser, end_tokens: [:when, :else] 981 | AST::SimpleWhen.new(expr_n, body, when_t.start_pos, body.last&.end_pos || expr_n.end_pos) 982 | end 983 | end 984 | 985 | class CaseElseParser < Parser 986 | def self.can_parse?(_self) 987 | _self.current_token&.type == :else 988 | end 989 | 990 | def parse! 991 | else_t = consume! :else 992 | body = consume_parser! ProgramParser 993 | AST::CaseElse.new(body, else_t.start_pos, body.last&.end_pos || else_t.end_pos) 994 | end 995 | end 996 | 997 | class WhenParser < Parser 998 | PARSERS = [SimpleWhenParser, CaseElseParser] 999 | 1000 | def self.can_parse?(_self) 1001 | PARSERS.any? { |klass| klass.can_parse? _self } 1002 | end 1003 | 1004 | def parse! 1005 | consume_first_valid_parser! PARSERS 1006 | end 1007 | end 1008 | 1009 | class EmptyCaseExprParser < Parser 1010 | def self.can_parse?(_self) 1011 | _self.current_token&.type == :case && 1012 | _self.new_line?(1) 1013 | end 1014 | 1015 | def parse! 1016 | case_t = consume! :case 1017 | cases = [] 1018 | loop do 1019 | break if current_token&.type == :end 1020 | cases.push consume_parser! WhenParser 1021 | end 1022 | end_t = consume! :end 1023 | 1024 | AST::EmptyCaseExpr.new(cases, case_t.start_pos, end_t.end_pos) 1025 | end 1026 | end 1027 | 1028 | ### CASE EXPRESSIONS END 1029 | 1030 | ### FOR LOOPS START 1031 | 1032 | class SimpleForOfLoopParser < Parser 1033 | def self.can_parse?(_self) 1034 | _self.current_token&.type == :for && 1035 | _self.peek_token.type == :identifier && 1036 | _self.peek_token_twice.type == :of 1037 | end 1038 | 1039 | def parse! 1040 | for_t = consume! :for 1041 | iter_var_t = consume! :identifier 1042 | consume! :of 1043 | arr_expr_n = consume_parser! ExprParser 1044 | body = consume_parser! ProgramParser 1045 | end_t = consume! :end 1046 | AST::SimpleForOfLoop.new(iter_var_t.value, arr_expr_n, body, for_t.start_pos, end_t.end_pos) 1047 | end 1048 | end 1049 | 1050 | class ForOfObjDeconstructLoopParser < Parser 1051 | def self.can_parse?(_self) 1052 | _self.current_token&.type == :for && 1053 | _self.peek_token.type == :"{" 1054 | end 1055 | 1056 | def parse! 1057 | for_t = consume! :for 1058 | properties = [] 1059 | consume! :"{" 1060 | loop do 1061 | properties.push consume!(:identifier).value 1062 | break if current_token.type == :"}" 1063 | consume! :comma 1064 | end 1065 | consume! :"}" 1066 | consume! :of 1067 | arr_expr_n = consume_parser! ExprParser 1068 | body = consume_parser! ProgramParser 1069 | end_t = consume! :end 1070 | AST::ForOfObjDeconstructLoop.new(properties, arr_expr_n, body, for_t.start_pos, end_t.end_pos) 1071 | end 1072 | end 1073 | 1074 | class SimpleForInLoopParser < Parser 1075 | def self.can_parse?(_self) 1076 | _self.current_token.type == :for && 1077 | _self.peek_token.type == :identifier && 1078 | _self.peek_token_twice.type == :in 1079 | end 1080 | 1081 | def parse! 1082 | for_t = consume! :for 1083 | variable_t = consume! :identifier 1084 | consume! :in 1085 | object_n = consume_parser! ExprParser 1086 | body = consume_parser! ProgramParser 1087 | end_t = consume! :end 1088 | AST::SimpleForInLoop.new( 1089 | variable_t.value, 1090 | object_n, 1091 | body, 1092 | for_t.start_pos, 1093 | end_t.end_pos 1094 | ) 1095 | end 1096 | end 1097 | 1098 | ### FOR LOOPS END 1099 | 1100 | ### XML START 1101 | 1102 | class SimpleElementParser < Parser 1103 | def self.can_parse?(_self) 1104 | _self.current_token&.type == :< && 1105 | _self.peek_token&.type == :identifier && 1106 | _self.peek_token_twice&.type == :> 1107 | end 1108 | 1109 | def parse! 1110 | open_t = consume! :< 1111 | name_t = consume! :identifier 1112 | consume! :> 1113 | children = [] 1114 | loop do 1115 | break if !ElementParser.can_parse?(self) 1116 | children.push consume_parser! ElementParser 1117 | end 1118 | consume! :" 1121 | AST::SimpleElement.new( 1122 | name_t.value, 1123 | children, 1124 | open_t.start_pos, 1125 | close_t.end_pos 1126 | ) 1127 | end 1128 | end 1129 | 1130 | class EscapedElementExprParser < Parser 1131 | def self.can_parse?(_self) 1132 | _self.current_token&.type == :"{" 1133 | end 1134 | 1135 | def parse! 1136 | open_t = consume! :"{" 1137 | expr_n = consume_parser! ExprParser 1138 | close_t = consume! :"}" 1139 | AST::EscapedElementExpr.new( 1140 | expr_n, 1141 | open_t.start_pos, 1142 | close_t.end_pos 1143 | ) 1144 | end 1145 | end 1146 | 1147 | class ElementParser < Parser 1148 | PARSERS = [ 1149 | SimpleElementParser, 1150 | EscapedElementExprParser, 1151 | ] 1152 | 1153 | def self.can_parse?(_self) 1154 | PARSERS.any? { |klass| klass.can_parse? _self } 1155 | end 1156 | 1157 | def parse! 1158 | consume_first_valid_parser! PARSERS 1159 | end 1160 | end 1161 | 1162 | ### XML END 1163 | 1164 | class ThisParser < Parser 1165 | def self.can_parse?(_self) 1166 | _self.current_token&.type == :this 1167 | end 1168 | 1169 | def parse! 1170 | this_t = consume! :this 1171 | AST::This.new(this_t.start_pos, this_t.end_pos) 1172 | end 1173 | end 1174 | 1175 | class ConstructableParser < Parser 1176 | PARSERS = [ 1177 | IdentifierLookupParser, 1178 | ThisParser, 1179 | ] 1180 | 1181 | def self.can_parse?(_self) 1182 | PARSERS.any? { |klass| klass.can_parse? _self } 1183 | end 1184 | 1185 | def parse! 1186 | consume_first_valid_parser! PARSERS 1187 | end 1188 | end 1189 | 1190 | class NewParser < Parser 1191 | def self.can_parse?(_self) 1192 | _self.current_token&.type == :new 1193 | end 1194 | 1195 | def parse! 1196 | new_t = consume! :new 1197 | class_expr_n = consume_parser! ConstructableParser 1198 | open_p_t = consume! :open_paren 1199 | args = [] 1200 | loop do 1201 | args.push consume_parser! ExprParser 1202 | consume_if_present! :comma 1203 | break if current_token.type == :close_paren 1204 | end 1205 | close_p_t = consume! :close_paren 1206 | AST::New.new( 1207 | class_expr_n, 1208 | args, 1209 | new_t.start_pos, 1210 | close_p_t.end_pos 1211 | ) 1212 | end 1213 | end 1214 | 1215 | class DynamicLookupParser < Parser 1216 | def self.can_parse?(_self, lhs) 1217 | # a[1] is valid, but a [1] 1218 | _self.current_token&.type == :"[" && 1219 | _self.current_token.start_pos == _self.prev_token.end_pos 1220 | end 1221 | 1222 | def parse!(lhs) 1223 | open_t = consume! :"[" 1224 | expr_n = consume_parser! ExprParser 1225 | close_t = consume! :"]" 1226 | AST::DynamicLookup.new( 1227 | lhs, 1228 | expr_n, 1229 | open_t.start_pos, 1230 | close_t.end_pos 1231 | ) 1232 | end 1233 | end 1234 | 1235 | class SchemaCaptureParser < Parser 1236 | def self.can_parse?(_self) 1237 | _self.current_token&.type == :capture 1238 | end 1239 | 1240 | def parse! 1241 | id_t = consume! :capture 1242 | AST::SchemaCapture.new(id_t.value, id_t.start_pos, id_t.end_pos) 1243 | end 1244 | end 1245 | 1246 | ### RANGE START 1247 | 1248 | class RangeOperandParser < Parser 1249 | PARSERS = [ 1250 | IdentifierLookupParser, 1251 | IntParser, 1252 | FloatParser, 1253 | SimpleStringParser, 1254 | ] 1255 | 1256 | def self.can_parse?(_self) 1257 | PARSERS.any? { |klass| klass.can_parse _self } 1258 | end 1259 | 1260 | def parse! 1261 | consume_first_valid_parser! PARSERS 1262 | end 1263 | end 1264 | 1265 | class RangeParser < Parser 1266 | def self.can_parse?(_self, lhs_n) 1267 | _self.current_token&.type == :".." 1268 | end 1269 | 1270 | def parse!(lhs_n) 1271 | consume! :".." 1272 | rhs_n = consume_parser! RangeOperandParser 1273 | AST::Range.new(lhs_n, rhs_n, lhs_n.start_pos, rhs_n.end_pos) 1274 | end 1275 | end 1276 | 1277 | ### RANGE END 1278 | 1279 | ### SCHEMA START 1280 | 1281 | class SchemaObjectParser < Parser 1282 | def self.can_parse?(_self) 1283 | _self.current_token&.type == :"{" 1284 | end 1285 | 1286 | VALUE_PARSERS = [ 1287 | IntParser, 1288 | FloatParser, 1289 | SimpleStringParser, 1290 | ShortAnonFnParser, 1291 | SchemaCaptureParser, 1292 | BoolParser, 1293 | ] 1294 | 1295 | def parse_value!(key_name, start_pos, end_pos) 1296 | return AST::SchemaCapture.new(key_name, start_pos, end_pos) if current_token.type != :colon 1297 | consume! :colon 1298 | consume_first_valid_parser! VALUE_PARSERS 1299 | end 1300 | 1301 | def parse! 1302 | open_b_t = consume! :"{" 1303 | properties = [] 1304 | loop do 1305 | property_t = consume! :identifier 1306 | properties.push [property_t.value, parse_value!(property_t.value, property_t.start_pos, property_t.end_pos)] 1307 | break if current_token.type == :"}" 1308 | consume! :comma 1309 | end 1310 | end_b_t = consume! :"}" 1311 | AST::SchemaObjectLiteral.new(properties, open_b_t.start_pos, end_b_t.end_pos) 1312 | end 1313 | end 1314 | 1315 | class SchemaExprParser < Parser 1316 | SCHEMA_PARSERS = [ 1317 | SchemaObjectParser, 1318 | IntParser, 1319 | SimpleStringParser, 1320 | IdentifierLookupParser, 1321 | BoolParser, 1322 | ShortAnonFnParser, 1323 | NullSchemaParser, 1324 | ] 1325 | 1326 | def parse! 1327 | consume_first_valid_parser! SCHEMA_PARSERS 1328 | end 1329 | end 1330 | 1331 | class SchemaUnionParser < Parser 1332 | def self.can_parse?(_self) 1333 | _self.current_token&.type == :"|" 1334 | end 1335 | 1336 | def parse!(first_schema_expr_n) 1337 | schema_exprs = [first_schema_expr_n] 1338 | loop do 1339 | break if !self.class.can_parse?(self) 1340 | consume! :"|" 1341 | schema_exprs.push consume_parser! SchemaExprParser 1342 | end 1343 | 1344 | AST::SchemaUnion.new(schema_exprs, first_schema_expr_n.start_pos, schema_exprs[-1].end_pos) 1345 | end 1346 | end 1347 | 1348 | class SchemaIntersectParser < Parser 1349 | def self.can_parse?(_self) 1350 | _self.current_token&.type == :"&" 1351 | end 1352 | 1353 | def parse!(first_schema_expr_n) 1354 | schema_exprs = [first_schema_expr_n] 1355 | 1356 | loop do 1357 | break if !self.class.can_parse?(self) 1358 | consume! :"&" 1359 | schema_exprs.push consume_parser! SchemaExprParser 1360 | end 1361 | 1362 | AST::SchemaIntersect.new(schema_exprs, first_schema_expr_n.start_pos, schema_exprs[-1].end_pos) 1363 | end 1364 | end 1365 | 1366 | class SchemaDefinitionParser < Parser 1367 | def self.can_parse?(_self) 1368 | _self.current_token&.type == :schema 1369 | end 1370 | 1371 | OP_PARSERS = [ 1372 | SchemaUnionParser, 1373 | SchemaIntersectParser, 1374 | ] 1375 | 1376 | def parse! 1377 | schema_t = consume! :schema 1378 | name_t = consume! :identifier 1379 | consume! :"=" 1380 | expr_n = consume_parser! SchemaExprParser 1381 | 1382 | loop do 1383 | op_parser_klass = OP_PARSERS.find do |parser_klass| 1384 | parser_klass.can_parse?(self) 1385 | end 1386 | break if !op_parser_klass 1387 | expr_n = consume_parser! op_parser_klass, expr_n 1388 | end 1389 | 1390 | AST::SchemaDefinition.new(name_t.value, expr_n, schema_t.start_pos, expr_n.end_pos) 1391 | end 1392 | end 1393 | 1394 | class SimpleSchemaAssignmentParser < Parser 1395 | def self.can_parse?(_self) 1396 | _self.current_token&.type == :identifier && 1397 | _self.peek_token.type == :open_paren && 1398 | _self.peek_token_twice.type == :identifier && 1399 | _self.peek_token_thrice.type == :close_paren 1400 | end 1401 | 1402 | def parse! 1403 | schema_name_t = consume! :identifier 1404 | consume! :open_paren 1405 | var_t = consume! :identifier 1406 | consume! :close_paren 1407 | consume! :assign 1408 | expr_n = consume_parser! ExprParser 1409 | AST::SimpleSchemaAssignment.new( 1410 | schema_name_t.value, 1411 | var_t.value, 1412 | expr_n, 1413 | schema_name_t.start_pos, 1414 | expr_n.end_pos 1415 | ) 1416 | end 1417 | end 1418 | 1419 | class ThisSchemaArgParser < Parser 1420 | def self.can_parse?(_self) 1421 | _self.rest_of_line.include?("::") 1422 | end 1423 | 1424 | def parse! 1425 | schema = consume_parser! SchemaExprParser 1426 | bind_t = consume! :"::" 1427 | AST::ThisSchemaArg.new(schema, schema.start_pos, bind_t.end_pos) 1428 | end 1429 | end 1430 | 1431 | class SpreadExprParser < Parser 1432 | def self.can_parse?(_self) 1433 | _self.current_token&.type == :"..." 1434 | end 1435 | 1436 | def parse! 1437 | spread_t = consume! :"..." 1438 | expr_n = consume_parser! ExprParser 1439 | 1440 | AST::SpreadExpr.new(expr_n, spread_t.start_pos, expr_n.end_pos) 1441 | end 1442 | end 1443 | 1444 | ### SCHEMA END 1445 | 1446 | class EnumExpr < Parser 1447 | def self.can_parse?(_self) 1448 | _self.current_token&.type == :identifier && 1449 | _self.peek_token&.type == :hash_id 1450 | end 1451 | 1452 | def parse! 1453 | enum_name_t = consume! :identifier 1454 | variant_t = consume! :hash_id 1455 | AST::EnumExpr.new(enum_name_t.value, variant_t.value, enum_name_t.start_pos, variant_t.end_pos) 1456 | end 1457 | end 1458 | 1459 | class ExprParser < Parser 1460 | # order matters 1461 | PRIMARY_PARSERS = [ 1462 | EnumExpr, 1463 | NullParser, 1464 | IntParser, 1465 | FloatParser, 1466 | ThisParser, 1467 | BoolParser, 1468 | SimpleStringParser, 1469 | AnonIdLookupParser, 1470 | NewParser, 1471 | ArrayParser, 1472 | ObjectParser, 1473 | MultiLineArrowFnWithArgsParser, 1474 | SingleLineArrowFnWithOneArgParser, 1475 | IdentifierLookupParser, 1476 | ShortAnonFnWithNamedArgParser, 1477 | ShortAnonFnParser, 1478 | SingleLineArrowFnWithoutArgsParser, 1479 | SingleLineArrowFnWithArgsParser, 1480 | AwaitParser, 1481 | SpreadExprParser, 1482 | ElementParser, 1483 | SchemaCaptureParser, 1484 | EmptyCaseExprParser, 1485 | NotParser, 1486 | ] 1487 | 1488 | SECONDARY_PARSERS = [ 1489 | RangeParser, 1490 | OperatorParser, 1491 | DotAssignmentParser, 1492 | DotParser, 1493 | BindParser, 1494 | OptionalChainParser, 1495 | DynamicLookupParser, 1496 | DefaultAssignmentParser, 1497 | PlusAssignmentParser, 1498 | FunctionCallWithoutArgs, 1499 | FunctionCallWithArgs, 1500 | FunctionCallWithArgsWithoutParens, 1501 | ] 1502 | 1503 | def parse! 1504 | expr_n = consume_first_valid_parser! PRIMARY_PARSERS do 1505 | binding.pry 1506 | end 1507 | loop do 1508 | secondary_klass = SECONDARY_PARSERS.find { |parser_klass| parser_klass.can_parse?(self, expr_n) } 1509 | break if !secondary_klass 1510 | expr_n = consume_parser! secondary_klass, expr_n 1511 | end 1512 | expr_n 1513 | end 1514 | end 1515 | 1516 | class CaseFunctionParser < Parser 1517 | def self.can_parse?(_self) 1518 | _self.current_token.type == :case && 1519 | _self.peek_token.type == :function 1520 | end 1521 | 1522 | def parse_case! 1523 | when_t = consume! :when 1524 | this_schema_arg = consume_parser! ThisSchemaArgParser if ThisSchemaArgParser.can_parse? self 1525 | has_open_paren = !!consume!(:open_paren) if current_token.type == :open_paren 1526 | arg_patterns = [] 1527 | loop do 1528 | break if current_token.type == :close_paren 1529 | arg_patterns.push consume_parser! SchemaArgParser 1530 | break if current_token.type == :close_paren 1531 | break if !has_open_paren 1532 | consume! :comma 1533 | end 1534 | consume! :close_paren if has_open_paren 1535 | body_n = consume_parser! FunctionBodyParser, end_tokens: [:when] 1536 | AST::CaseFnPattern.new(this_schema_arg, arg_patterns, body_n, when_t.start_pos, body_n[-1].end_pos) 1537 | end 1538 | 1539 | def parse! 1540 | case_t = consume! :case 1541 | consume! :function 1542 | name_t = consume! :identifier 1543 | patterns = [] 1544 | while current_token.type != :end 1545 | patterns.push parse_case! 1546 | end 1547 | end_t = consume! :end 1548 | AST::CaseFunctionDefinition.new( 1549 | name_t.value, 1550 | patterns, 1551 | case_t.start_pos, 1552 | end_t.end_pos 1553 | ) 1554 | end 1555 | end 1556 | 1557 | class SingleLineBindFunctionDefinitionParser < Parser 1558 | def self.can_parse?(_self) 1559 | _self.current_token&.type == :function && 1560 | _self.peek_token&.type == :identifier && 1561 | _self.peek_token_twice&.type == :"::" && 1562 | _self.rest_of_line.include?("=") 1563 | end 1564 | 1565 | def parse! 1566 | fn_t = consume! :function 1567 | obj_name_t = consume! :identifier 1568 | consume! :"::" 1569 | fn_name_t = consume! :identifier 1570 | args = consume_parser! SimpleFnArgsParser 1571 | consume! :"=" 1572 | return_expr_n = consume_parser! ExprParser 1573 | AST::SingleLineBindFunctionDefinition.new( 1574 | obj_name_t.value, 1575 | fn_name_t.value, 1576 | args, 1577 | return_expr_n, 1578 | fn_t.start_pos, 1579 | return_expr_n.end_pos 1580 | ) 1581 | end 1582 | end 1583 | 1584 | class MultiLineBindFunctionDefinitionParser < Parser 1585 | def self.can_parse?(_self) 1586 | _self.current_token&.type == :function && 1587 | _self.peek_token&.type == :identifier && 1588 | _self.peek_token_twice&.type == :"::" && 1589 | !_self.rest_of_line.include?("=") 1590 | end 1591 | 1592 | def parse! 1593 | fn_t = consume! :function 1594 | obj_name_t = consume! :identifier 1595 | consume! :"::" 1596 | fn_name_t = consume! :identifier 1597 | args = consume_parser! SimpleFnArgsParser 1598 | body = consume_parser! FunctionBodyParser 1599 | end_t = consume! :end 1600 | AST::MultiLineBindFunctionDefinition.new( 1601 | obj_name_t.value, 1602 | fn_name_t.value, 1603 | args, 1604 | body, 1605 | fn_t.start_pos, 1606 | end_t.end_pos 1607 | ) 1608 | end 1609 | end 1610 | 1611 | class FunctionDefinitionParser < Parser 1612 | PARSERS = [ 1613 | SingleLineDefWithArgsParser, 1614 | SingleLineBindFunctionDefinitionParser, 1615 | MultiLineBindFunctionDefinitionParser, 1616 | SingleLineDefWithoutArgsParser, 1617 | MultilineDefWithArgsParser, 1618 | MultilineDefWithoutArgsParser, 1619 | CaseFunctionParser, 1620 | ] 1621 | 1622 | def self.can_parse?(_self) 1623 | PARSERS.find { |klass| klass.can_parse? _self } 1624 | end 1625 | 1626 | def parse! 1627 | consume_first_valid_parser! PARSERS 1628 | end 1629 | end 1630 | 1631 | ### COMPONENTS START 1632 | 1633 | class BodyComponentWithoutAttrsParser < Parser 1634 | def self.can_parse?(_self) 1635 | _self.current_token.type == :component 1636 | end 1637 | 1638 | def parse! 1639 | component_t = consume! :component 1640 | name_t = consume! :identifier 1641 | constructor_body = consume_parser! ComponentConstructorParser 1642 | assert { constructor_body.all? { |node| node.is_a? AST::SimpleAssignment } } 1643 | consume! :in 1644 | expr_n = consume_parser! ExprParser 1645 | end_t = consume! :end 1646 | AST::BodyComponentWithoutAttrs.new( 1647 | name_t.value, 1648 | constructor_body, 1649 | expr_n, 1650 | component_t.start_pos, 1651 | end_t.end_pos 1652 | ) 1653 | end 1654 | end 1655 | 1656 | class SimpleComponentWithAttrsParser < Parser 1657 | def self.can_parse?(_self) 1658 | _self.current_token.type == :component && 1659 | _self.peek_token_twice.type == :"{" && 1660 | _self.rest_of_line.include?("in") 1661 | end 1662 | 1663 | def parse! 1664 | component_t = consume! :component 1665 | name_t = consume! :identifier 1666 | attributes = consume_parser! SchemaObjectParser 1667 | consume! :in 1668 | expr_n = consume_parser! ExprParser 1669 | end_t = consume! :end 1670 | AST::ExprComponentWithAttributes.new( 1671 | name_t.value, 1672 | attributes, 1673 | expr_n, 1674 | component_t.start_pos, 1675 | end_t.end_pos 1676 | ) 1677 | end 1678 | end 1679 | 1680 | class SimpleComponentParser < Parser 1681 | def self.can_parse?(_self) 1682 | _self.current_token.type == :component && 1683 | _self.peek_token_twice.type == :in 1684 | end 1685 | 1686 | def parse! 1687 | component_t = consume! :component 1688 | name_t = consume! :identifier 1689 | consume! :in 1690 | expr_n = consume_parser! ExprParser 1691 | end_t = consume! :end 1692 | AST::ExprComponent.new( 1693 | name_t.value, 1694 | expr_n, 1695 | component_t.start_pos, 1696 | end_t.end_pos 1697 | ) 1698 | end 1699 | end 1700 | 1701 | ### COMPONENTS END 1702 | 1703 | ### CLASSES START 1704 | 1705 | class ConstructorWithoutArgsParser < Parser 1706 | def self.can_parse?(_self) 1707 | _self.current_token&.type == :function && 1708 | _self.peek_token&.value == "constructor" 1709 | end 1710 | 1711 | def parse! 1712 | function_t = consume! :function 1713 | fn_name_t = consume! :identifier 1714 | body = consume_parser! ProgramParser 1715 | end_t = consume! :end 1716 | AST::ConstructorWithoutArgs.new( 1717 | fn_name_t.value, 1718 | body, 1719 | function_t.start_pos, 1720 | end_t.end_pos 1721 | ) 1722 | end 1723 | end 1724 | 1725 | class ConstructorWithArgsParser < Parser 1726 | def self.can_parse?(_self) 1727 | _self.current_token&.type == :function && 1728 | _self.peek_token&.value == "constructor" && 1729 | _self.peek_token_twice.type == :open_paren 1730 | end 1731 | 1732 | def parse! 1733 | function_t = consume! :function 1734 | fn_name_t = consume! :identifier 1735 | args_n = consume_parser! SimpleFnArgsParser 1736 | body = consume_parser! ProgramParser 1737 | end_t = consume! :end 1738 | AST::ConstructorWithArgs.new( 1739 | fn_name_t.value, 1740 | args_n, 1741 | body, 1742 | function_t.start_pos, 1743 | end_t.end_pos 1744 | ) 1745 | end 1746 | end 1747 | 1748 | class StaticMethodWithArgsParser < Parser 1749 | def self.can_parse?(_self) 1750 | _self.current_token.type == :static && 1751 | _self.peek_token.type == :function 1752 | end 1753 | 1754 | def parse! 1755 | static_t = consume! :static 1756 | consume! :function 1757 | name_t = consume! :identifier 1758 | args_n = consume_parser! SimpleFnArgsParser 1759 | body = consume_parser! FunctionBodyParser 1760 | end_t = consume! :end 1761 | AST::StaticMethod.new( 1762 | name_t.value, 1763 | args_n, 1764 | body, 1765 | static_t.start_pos, 1766 | end_t.end_pos 1767 | ) 1768 | end 1769 | end 1770 | 1771 | class DefaultConstructorArgParser < Parser 1772 | def self.can_parse?(_self) 1773 | _self.current_token&.type == :"@" && 1774 | _self.peek_token&.type == :identifier && 1775 | _self.peek_token_twice&.type == :"=" 1776 | end 1777 | 1778 | def parse! 1779 | at_t = consume! :"@" 1780 | name_t = consume! :identifier 1781 | consume! :"=" 1782 | expr_n = consume_parser! ExprParser 1783 | 1784 | AST::DefaultConstructorArg.new(name_t.value, expr_n, at_t.start_pos, expr_n.end_pos) 1785 | end 1786 | end 1787 | 1788 | class SimpleConstructorArgParser < Parser 1789 | def self.can_parse?(_self) 1790 | _self.current_token&.type == :"@" && 1791 | _self.peek_token&.type == :identifier 1792 | end 1793 | 1794 | def parse! 1795 | at_t = consume! :"@" 1796 | name_t = consume! :identifier 1797 | 1798 | AST::SimpleConstructorArg.new(name_t.value, at_t.start_pos, name_t.end_pos) 1799 | end 1800 | end 1801 | 1802 | class ConstructorArgParser < Parser 1803 | PARSERS = [ 1804 | DefaultConstructorArgParser, 1805 | SimpleConstructorArgParser, 1806 | ] 1807 | 1808 | def self.can_parse?(_self) 1809 | PARSERS.any? { |klass| klass.can_parse? _self } 1810 | end 1811 | 1812 | def parse! 1813 | consume_first_valid_parser! PARSERS 1814 | end 1815 | end 1816 | 1817 | class ShortHandConstructorParser < Parser 1818 | def self.can_parse?(_self) 1819 | _self.current_token.type == :function && 1820 | _self.peek_token.value == "constructor" && 1821 | _self.peek_token_thrice.type == :"@" 1822 | end 1823 | 1824 | def parse! 1825 | function_t = consume! :function 1826 | consume! :identifier 1827 | consume! :open_paren 1828 | args = [] 1829 | loop do 1830 | args.push consume_parser! ConstructorArgParser 1831 | break if current_token.type == :close_paren 1832 | consume! :comma 1833 | end 1834 | close_t = consume! :close_paren 1835 | AST::ShortHandConstructor.new( 1836 | args, 1837 | function_t.start_pos, 1838 | close_t.end_pos 1839 | ) 1840 | end 1841 | end 1842 | 1843 | class OneLineGetterParser < Parser 1844 | def self.can_parse?(_self) 1845 | _self.current_token.type == :get && 1846 | _self.peek_token.type == :identifier && 1847 | _self.peek_token_twice.type == :"=" 1848 | end 1849 | 1850 | def parse! 1851 | get_t = consume! :get 1852 | name_t = consume! :identifier 1853 | consume! :"=" 1854 | expr_n = consume_parser! ExprParser 1855 | AST::OneLineGetter.new( 1856 | name_t.value, 1857 | expr_n, 1858 | get_t.start_pos, 1859 | expr_n.end_pos 1860 | ) 1861 | end 1862 | end 1863 | 1864 | class InstancePropertyParser < Parser 1865 | def self.can_parse?(_self) 1866 | _self.current_token.type == :identifier && 1867 | _self.peek_token.type == :assign 1868 | end 1869 | 1870 | def parse! 1871 | id_t = consume! :identifier 1872 | consume! :assign 1873 | expr_n = consume_parser! ExprParser 1874 | AST::InstanceProperty.new( 1875 | id_t.value, 1876 | expr_n, 1877 | id_t.start_pos, 1878 | expr_n.end_pos 1879 | ) 1880 | end 1881 | end 1882 | 1883 | class ClassParser < Parser 1884 | def self.can_parse?(_self) 1885 | _self.current_token.type == :class 1886 | end 1887 | 1888 | PARSERS = [ 1889 | InstancePropertyParser, 1890 | ShortHandConstructorParser, 1891 | ConstructorWithArgsParser, 1892 | ConstructorWithoutArgsParser, 1893 | StaticMethodWithArgsParser, 1894 | OneLineGetterParser, 1895 | FunctionDefinitionParser, 1896 | ] 1897 | 1898 | def parse_parent_class! 1899 | return nil if current_token.type != :< 1900 | consume! :< 1901 | parent_class_t = consume! :identifier 1902 | return parent_class_t.value 1903 | end 1904 | 1905 | def parse! 1906 | class_t = consume! :class 1907 | class_name_t = consume! :identifier 1908 | parent_class = parse_parent_class! 1909 | entries = [] 1910 | loop do 1911 | break if !PARSERS.any? { |klass| klass.can_parse? self } 1912 | entries.push consume_first_valid_parser! PARSERS 1913 | end 1914 | end_t = consume! :end 1915 | AST::Class.new( 1916 | class_name_t.value, 1917 | parent_class, 1918 | entries, 1919 | class_t.start_pos, 1920 | end_t.end_pos 1921 | ) 1922 | end 1923 | end 1924 | 1925 | ### CLASSES END 1926 | 1927 | ### IF STATEMENTS START 1928 | 1929 | class ElseIfParser < Parser 1930 | def self.can_parse?(_self) 1931 | _self.current_token&.type == :else && 1932 | _self.peek_token&.type == :if 1933 | end 1934 | 1935 | def parse! 1936 | else_t = consume! :else 1937 | consume! :if 1938 | cond_n = consume_parser! ExprParser 1939 | body_n = consume_parser! ProgramParser, end_tokens: [:else] 1940 | AST::ElseIf.new(cond_n, body_n, else_t.start_pos, body_n.last&.end_pos) 1941 | end 1942 | end 1943 | 1944 | class ElseParser < Parser 1945 | def self.can_parse?(_self) 1946 | _self.current_token&.type == :else 1947 | end 1948 | 1949 | def parse! 1950 | else_t = consume! :else 1951 | body_n = consume_parser! ProgramParser 1952 | AST::Else.new(body_n, else_t.start_pos, body_n.last&.end_pos) 1953 | end 1954 | end 1955 | 1956 | class ElseBranchParser < Parser 1957 | PARSERS = [ 1958 | ElseIfParser, 1959 | ElseParser, 1960 | ] 1961 | 1962 | def self.can_parse?(_self) 1963 | PARSERS.any? { |klass| klass.can_parse? _self } 1964 | end 1965 | 1966 | def parse! 1967 | consume_first_valid_parser! PARSERS 1968 | end 1969 | end 1970 | 1971 | class IfParser < Parser 1972 | def self.can_parse?(_self) 1973 | _self.current_token&.type == :if 1974 | end 1975 | 1976 | def parse! 1977 | if_t = consume! :if 1978 | cond_n = consume_parser! ExprParser 1979 | pass_n = consume_parser! ProgramParser, end_tokens: [:else] 1980 | branches = [] 1981 | loop do 1982 | break if !ElseBranchParser.can_parse?(self) 1983 | branches.push consume_parser! ElseBranchParser 1984 | end 1985 | end_t = consume! :end 1986 | AST::If.new(cond_n, pass_n, branches, if_t.start_pos, end_t.end_pos) 1987 | end 1988 | end 1989 | 1990 | ### IF STATEMENTS END 1991 | 1992 | class EnumParser < Parser 1993 | def self.can_parse?(_self) 1994 | _self.current_token&.type == :enum 1995 | end 1996 | 1997 | def parse! 1998 | enum_t = consume! :enum 1999 | name_t = consume! :identifier 2000 | consume! :"=" 2001 | variants = [] 2002 | loop do 2003 | variant_t = consume! :identifier 2004 | variants.push(variant_t) 2005 | break if current_token&.type != :| 2006 | consume! :| 2007 | end 2008 | AST::OneLineEnum.new(name_t.value, variants.map(&:value), enum_t.start_pos, variants[-1].end_pos) 2009 | end 2010 | end 2011 | 2012 | class ProgramParser < Parser 2013 | def initialize(*args) 2014 | super(*args) 2015 | @body = [] 2016 | end 2017 | 2018 | ALLOWED_PARSERS = [ 2019 | IfParser, 2020 | FunctionDefinitionParser, 2021 | SimpleAssignmentParser, 2022 | SimpleReassignmentParser, 2023 | ArrayAssignmentParser, 2024 | ForOfObjDeconstructLoopParser, 2025 | SimpleForInLoopParser, 2026 | SimpleForOfLoopParser, 2027 | SchemaDefinitionParser, 2028 | SimpleSchemaAssignmentParser, 2029 | SimpleComponentWithAttrsParser, 2030 | SimpleComponentParser, 2031 | BodyComponentWithoutAttrsParser, 2032 | ClassParser, 2033 | ReturnParser, 2034 | EnumParser, 2035 | ] 2036 | 2037 | def consume_parser!(parser_klass) 2038 | expr_n = super parser_klass 2039 | @body.push expr_n 2040 | end 2041 | 2042 | def parse!(additional_parsers: [], end_tokens: []) 2043 | while current_token&.is_not_one_of?(*end_tokens, :end, :"}") 2044 | klass = (ALLOWED_PARSERS + additional_parsers).find { |klass| klass.can_parse?(self) } 2045 | 2046 | if !klass 2047 | klass = ExprParser 2048 | end 2049 | 2050 | expr_n = consume_parser! klass 2051 | break if !expr_n 2052 | end 2053 | 2054 | @body 2055 | end 2056 | end 2057 | 2058 | class ComponentConstructorParser < ProgramParser 2059 | def parse! 2060 | super end_tokens: [:in] 2061 | end 2062 | end 2063 | 2064 | class FunctionBodyParser < ProgramParser 2065 | def parse!(end_tokens: []) 2066 | super additional_parsers: ALLOWED_PARSERS, end_tokens: end_tokens 2067 | return [] if @body.size == 0 2068 | 2069 | last_n = @body[-1] 2070 | if last_n.is_a? AST::SimpleAssignment 2071 | @body.push AST::Return.new( 2072 | AST::IdLookup.new(last_n.name, last_n.start_pos, last_n.end_pos), 2073 | last_n.start_pos, 2074 | last_n.end_pos 2075 | ) 2076 | elsif last_n.is_a? AST::EmptyCaseExpr 2077 | last_n.cases.each do |case_| 2078 | next if case_.body.size == 0 2079 | case_.body[-1] = AST::Return.new(case_.body[-1], case_.body[-1].start_pos, case_.body[-1].end_pos) 2080 | end 2081 | elsif last_n.is_not_one_of? AST::Return, AST::SimpleForOfLoop, AST::SimpleForInLoop, AST::ForOfObjDeconstructLoop 2082 | @body[-1] = AST::Return.new(last_n, last_n.start_pos, last_n.end_pos) 2083 | end 2084 | 2085 | @body 2086 | end 2087 | end 2088 | --------------------------------------------------------------------------------