kapusta 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +50 -11
- data/bin/check-all +6 -1
- data/bin/compile-examples +7 -2
- data/examples/anagram.kap +3 -3
- data/examples/app/args.kap +9 -0
- data/examples/arrange-coins.kap +3 -3
- data/examples/baseball-game.kap +5 -5
- data/examples/best-time-to-buy-sell-stock.kap +2 -2
- data/examples/binary-search.kap +2 -2
- data/examples/binary-to-decimal.kap +1 -1
- data/examples/blocks-and-kwargs.kap +2 -2
- data/examples/count-effects.kap +1 -1
- data/examples/count-items-matching-rule.kap +18 -0
- data/examples/doto-hygiene.kap +2 -2
- data/examples/doto.kap +2 -2
- data/examples/egg-count.kap +1 -1
- data/examples/exceptions.kap +1 -1
- data/examples/falling-drops.kap +7 -7
- data/examples/fennel-parity-examples.txt +3 -6
- data/examples/good-pairs.kap +18 -0
- data/examples/greet.kap +1 -1
- data/examples/happy-number.kap +2 -2
- data/examples/left-right-difference.kap +60 -0
- data/examples/length-of-last-word.kap +4 -4
- data/examples/manhattan-distance.kap +1 -1
- data/examples/maximum-subarray.kap +23 -7
- data/examples/minimum-start-value.kap +19 -0
- data/examples/move-zeroes.kap +2 -2
- data/examples/mruby-runtime-examples.txt +8 -2
- data/examples/non-constant-local.kap +1 -1
- data/examples/number-of-1-bits.kap +1 -1
- data/examples/number-of-steps.kap +1 -1
- data/examples/palindrome.kap +2 -2
- data/examples/pangram.kap +4 -4
- data/examples/pcall.kap +3 -3
- data/examples/pipeline.kap +4 -4
- data/examples/plus-one.kap +3 -3
- data/examples/raindrops.kap +1 -1
- data/examples/range-width.kap +14 -0
- data/examples/recent-counter.kap +6 -6
- data/examples/require-local-args.kap +9 -0
- data/examples/require-local.kap +7 -0
- data/examples/require-module-local.kap +4 -0
- data/examples/require-module.kap +4 -0
- data/examples/reverse-integer.kap +1 -1
- data/examples/roman-to-integer.kap +3 -3
- data/examples/running-sum.kap +20 -0
- data/examples/safe-lookup.kap +2 -2
- data/examples/single-number.kap +1 -1
- data/examples/stack.kap +14 -14
- data/examples/subtract-product-sum.kap +1 -1
- data/examples/summary-ranges.kap +45 -0
- data/examples/threading.kap +5 -5
- data/examples/tset.kap +1 -1
- data/examples/two-sum-hash.kap +3 -3
- data/examples/two-sum.kap +1 -1
- data/examples/ugly-number.kap +1 -1
- data/examples/underground-system.kap +39 -0
- data/examples/valid-parentheses-1.kap +6 -6
- data/exe/kapusta-ls +49 -2
- data/lib/kapusta/ast.rb +8 -0
- data/lib/kapusta/compiler/emitter/bindings.rb +111 -89
- data/lib/kapusta/compiler/emitter/collections.rb +32 -40
- data/lib/kapusta/compiler/emitter/control_flow.rb +33 -31
- data/lib/kapusta/compiler/emitter/expressions.rb +21 -5
- data/lib/kapusta/compiler/emitter/interop.rb +168 -48
- data/lib/kapusta/compiler/emitter/patterns.rb +12 -14
- data/lib/kapusta/compiler/emitter/support.rb +63 -81
- data/lib/kapusta/compiler/language.rb +522 -0
- data/lib/kapusta/compiler/lua_compat.rb +23 -28
- data/lib/kapusta/compiler/macro_expander.rb +30 -30
- data/lib/kapusta/compiler/macro_lowerer.rb +12 -24
- data/lib/kapusta/compiler/normalizer.rb +25 -17
- data/lib/kapusta/compiler.rb +3 -24
- data/lib/kapusta/env.rb +2 -2
- data/lib/kapusta/errors.rb +2 -1
- data/lib/kapusta/formatter/ast_helpers.rb +78 -0
- data/lib/kapusta/formatter/cli.rb +125 -0
- data/lib/kapusta/formatter/line_helpers.rb +44 -0
- data/lib/kapusta/formatter/validator.rb +32 -0
- data/lib/kapusta/formatter.rb +354 -325
- data/lib/kapusta/lsp/identifier.rb +1 -1
- data/lib/kapusta/lsp/rename.rb +21 -11
- data/lib/kapusta/lsp/scope_walker.rb +122 -212
- data/lib/kapusta/lsp/workspace_index.rb +17 -5
- data/lib/kapusta/reader.rb +4 -2
- data/lib/kapusta/version.rb +1 -1
- data/lib/kapusta.rb +39 -6
- data/spec/cli_spec.rb +13 -0
- data/spec/examples_errors_spec.rb +3 -1
- data/spec/examples_spec.rb +67 -15
- data/spec/formatter_spec.rb +246 -0
- data/spec/lsp_spec.rb +69 -0
- data/spec/require_spec.rb +294 -0
- metadata +20 -2
- data/examples/describe.kap +0 -9
|
@@ -32,18 +32,16 @@ module Kapusta
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def expand_top(form)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return []
|
|
46
|
-
end
|
|
35
|
+
case Language.list_head_name(form)
|
|
36
|
+
when 'macro'
|
|
37
|
+
register_macro_form(form.rest)
|
|
38
|
+
return []
|
|
39
|
+
when 'macros'
|
|
40
|
+
register_macros_form(form.rest)
|
|
41
|
+
return []
|
|
42
|
+
when 'import-macros'
|
|
43
|
+
handle_import_macros(form)
|
|
44
|
+
return []
|
|
47
45
|
end
|
|
48
46
|
[expand(form)]
|
|
49
47
|
end
|
|
@@ -104,11 +102,11 @@ module Kapusta
|
|
|
104
102
|
end
|
|
105
103
|
|
|
106
104
|
def register_macro_form(args)
|
|
107
|
-
|
|
108
|
-
raise macro_error(:macro_name_must_be_symbol,
|
|
109
|
-
raise macro_error(:macro_params_must_be_vector, params) unless params.is_a?(Vec)
|
|
105
|
+
parsed = Language.parse_macro_definition_args(args)
|
|
106
|
+
raise macro_error(:macro_name_must_be_symbol, parsed.name) unless parsed.name.is_a?(Sym)
|
|
107
|
+
raise macro_error(:macro_params_must_be_vector, parsed.params) unless parsed.params.is_a?(Vec)
|
|
110
108
|
|
|
111
|
-
register(
|
|
109
|
+
register(parsed.name.name, parsed.params, parsed.body)
|
|
112
110
|
end
|
|
113
111
|
|
|
114
112
|
def register_macros_form(args)
|
|
@@ -116,11 +114,15 @@ module Kapusta
|
|
|
116
114
|
raise macro_error(:macros_expects_hash, hash_lit) unless hash_lit.is_a?(HashLit)
|
|
117
115
|
|
|
118
116
|
hash_lit.pairs.each do |key, value|
|
|
119
|
-
|
|
117
|
+
unless Language.function_head?(Language.list_head_name(value))
|
|
118
|
+
raise macro_error(:macros_entry_must_be_fn, value, form: value.inspect)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
parsed = Language.parse_function_form(value)
|
|
120
122
|
|
|
121
123
|
name = key.to_s
|
|
122
|
-
params = value.items[1]
|
|
123
|
-
body = value.items[2..]
|
|
124
|
+
params = parsed&.named? ? value.items[1] : (parsed&.params || value.items[1])
|
|
125
|
+
body = parsed&.body || value.items[2..]
|
|
124
126
|
raise macro_error(:macros_entry_params_must_be_vector, params) unless params.is_a?(Vec)
|
|
125
127
|
|
|
126
128
|
register(name, params, body)
|
|
@@ -128,26 +130,24 @@ module Kapusta
|
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
def fn_form?(value)
|
|
131
|
-
|
|
133
|
+
Language.function_form?(value)
|
|
132
134
|
end
|
|
133
135
|
|
|
134
136
|
def handle_import_macros(form)
|
|
135
|
-
|
|
136
|
-
destructure
|
|
137
|
-
module_arg = args[1]
|
|
138
|
-
unless destructure.is_a?(HashLit) || destructure.is_a?(Sym)
|
|
137
|
+
parsed = Language.parse_import_macros_args(form.rest)
|
|
138
|
+
unless parsed.destructure.is_a?(HashLit) || parsed.destructure.is_a?(Sym)
|
|
139
139
|
raise macro_error(:import_macros_destructure_invalid, form)
|
|
140
140
|
end
|
|
141
|
-
unless module_arg.is_a?(Symbol) || module_arg.is_a?(String)
|
|
141
|
+
unless parsed.module_arg.is_a?(Symbol) || parsed.module_arg.is_a?(String)
|
|
142
142
|
raise macro_error(:import_macros_module_invalid, form)
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
-
module_label = MacroImporter.module_label(module_arg)
|
|
146
|
-
exports = macro_importer.load(module_arg, form)
|
|
147
|
-
if destructure.is_a?(HashLit)
|
|
148
|
-
register_imported_macros(destructure, exports, module_label, form)
|
|
145
|
+
module_label = MacroImporter.module_label(parsed.module_arg)
|
|
146
|
+
exports = macro_importer.load(parsed.module_arg, form)
|
|
147
|
+
if parsed.destructure.is_a?(HashLit)
|
|
148
|
+
register_imported_macros(parsed.destructure, exports, module_label, form)
|
|
149
149
|
else
|
|
150
|
-
register_whole_module(destructure, exports)
|
|
150
|
+
register_whole_module(parsed.destructure, exports)
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
153
|
|
|
@@ -5,8 +5,6 @@ require_relative 'macro_gensym'
|
|
|
5
5
|
module Kapusta
|
|
6
6
|
module Compiler
|
|
7
7
|
class MacroLowerer
|
|
8
|
-
FN_HEADS = %w[fn lambda λ].freeze
|
|
9
|
-
|
|
10
8
|
def self.compile(params:, body:, path:, error_class:)
|
|
11
9
|
callable = new(error_class:).callable_form(params, body)
|
|
12
10
|
ruby = Compiler.compile_forms([callable], path:)
|
|
@@ -56,21 +54,11 @@ module Kapusta
|
|
|
56
54
|
private
|
|
57
55
|
|
|
58
56
|
def lower_fn_form(form)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
name_sym = items[1]
|
|
62
|
-
params = items[2]
|
|
63
|
-
body = items[3..] || []
|
|
64
|
-
elsif items[1].is_a?(Vec)
|
|
65
|
-
name_sym = nil
|
|
66
|
-
params = items[1]
|
|
67
|
-
body = items[2..] || []
|
|
68
|
-
else
|
|
69
|
-
return form
|
|
70
|
-
end
|
|
57
|
+
parsed = Language.parse_function_form(form)
|
|
58
|
+
return form unless parsed
|
|
71
59
|
|
|
72
|
-
head_items =
|
|
73
|
-
List.new(head_items + lowered_body_with_gensyms(body))
|
|
60
|
+
head_items = parsed.named? ? [form.head, parsed.name, parsed.params] : [form.head, parsed.params]
|
|
61
|
+
List.new(head_items + lowered_body_with_gensyms(parsed.body))
|
|
74
62
|
end
|
|
75
63
|
|
|
76
64
|
def lowered_body_with_gensyms(body)
|
|
@@ -91,7 +79,7 @@ module Kapusta
|
|
|
91
79
|
end
|
|
92
80
|
|
|
93
81
|
def fn_form?(form)
|
|
94
|
-
|
|
82
|
+
Language.function_form?(form)
|
|
95
83
|
end
|
|
96
84
|
|
|
97
85
|
def lower_quasi(form)
|
|
@@ -147,8 +135,8 @@ module Kapusta
|
|
|
147
135
|
end
|
|
148
136
|
|
|
149
137
|
def lower_quasi_item(item)
|
|
150
|
-
if item.is_a?(Unquote) &&
|
|
151
|
-
inner = lower(
|
|
138
|
+
if item.is_a?(Unquote) && (unpack = Language.parse_unpack_call(item.form))
|
|
139
|
+
inner = lower(unpack.value)
|
|
152
140
|
List.new([Sym.new('.'), inner, 0])
|
|
153
141
|
else
|
|
154
142
|
lower_quasi(item)
|
|
@@ -159,13 +147,13 @@ module Kapusta
|
|
|
159
147
|
last = items.last
|
|
160
148
|
return unless last
|
|
161
149
|
return lower(last.form) if last.is_a?(UnquoteSplice)
|
|
162
|
-
return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
|
|
163
150
|
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
if last.is_a?(Unquote)
|
|
152
|
+
unpack = Language.parse_unpack_call(last.form)
|
|
153
|
+
return lower(unpack.value) if unpack
|
|
154
|
+
end
|
|
166
155
|
|
|
167
|
-
|
|
168
|
-
form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
|
|
156
|
+
nil
|
|
169
157
|
end
|
|
170
158
|
|
|
171
159
|
def gensym_local_for(prefix)
|
|
@@ -41,27 +41,27 @@ module Kapusta
|
|
|
41
41
|
|
|
42
42
|
case head.name
|
|
43
43
|
when 'when'
|
|
44
|
-
|
|
44
|
+
parsed = Language.parse_conditional_body_args(items[1..])
|
|
45
|
+
raise compiler_error(:when_no_body, list, form: head.name) unless parsed.body?
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
body = wrap_do(items[2..])
|
|
48
|
-
Kapusta.copy_position(List.new([Sym.new('if'), cond, body]), list)
|
|
47
|
+
Kapusta.copy_position(List.new([Sym.new('if'), parsed.condition, wrap_do(parsed.body)]), list)
|
|
49
48
|
when 'unless'
|
|
50
|
-
|
|
49
|
+
parsed = Language.parse_conditional_body_args(items[1..])
|
|
50
|
+
raise compiler_error(:when_no_body, list, form: head.name) unless parsed.body?
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Kapusta.copy_position(List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body]), list)
|
|
52
|
+
negated = List.new([Sym.new('not'), parsed.condition])
|
|
53
|
+
Kapusta.copy_position(List.new([Sym.new('if'), negated, wrap_do(parsed.body)]), list)
|
|
55
54
|
when 'tset'
|
|
56
|
-
|
|
55
|
+
parsed = Language.parse_tset_args(items[1..])
|
|
56
|
+
raise compiler_error(:tset_no_value, list) unless parsed
|
|
57
57
|
|
|
58
58
|
Kapusta.copy_position(
|
|
59
|
-
List.new([Sym.new('set'), List.new([Sym.new('
|
|
59
|
+
List.new([Sym.new('set'), List.new([Sym.new(':'), parsed.table, parsed.key]), parsed.value]),
|
|
60
60
|
list
|
|
61
61
|
)
|
|
62
62
|
when *LuaCompat::SPECIAL_FORMS
|
|
63
63
|
normalize_lua_compat_form(head.name, items)
|
|
64
|
-
when
|
|
64
|
+
when *Language::THREAD_HEADS
|
|
65
65
|
Kapusta.copy_position(normalize(thread(items[1..], head.name)), list)
|
|
66
66
|
when 'doto'
|
|
67
67
|
Kapusta.copy_position(normalize(doto(items[1..])), list)
|
|
@@ -85,8 +85,8 @@ module Kapusta
|
|
|
85
85
|
|
|
86
86
|
def thread(forms, kind)
|
|
87
87
|
value = forms.first
|
|
88
|
-
short =
|
|
89
|
-
position =
|
|
88
|
+
short = Language.short_pipeline_head?(kind)
|
|
89
|
+
position = Language.thread_first_head?(kind) ? :first : :last
|
|
90
90
|
|
|
91
91
|
return thread_short(forms, position) if short
|
|
92
92
|
|
|
@@ -124,9 +124,9 @@ module Kapusta
|
|
|
124
124
|
def thread_step(memo, form, position)
|
|
125
125
|
if form.is_a?(List)
|
|
126
126
|
if position == :first
|
|
127
|
-
|
|
127
|
+
prepend_call_arg(form, memo)
|
|
128
128
|
else
|
|
129
|
-
|
|
129
|
+
append_call_arg(form, memo)
|
|
130
130
|
end
|
|
131
131
|
else
|
|
132
132
|
List.new([form, memo])
|
|
@@ -144,19 +144,27 @@ module Kapusta
|
|
|
144
144
|
temp = gensym('doto')
|
|
145
145
|
body = forms[1..].map do |form|
|
|
146
146
|
if form.is_a?(List)
|
|
147
|
-
|
|
147
|
+
prepend_call_arg(form, temp)
|
|
148
148
|
else
|
|
149
149
|
List.new([form, temp])
|
|
150
150
|
end
|
|
151
151
|
end
|
|
152
152
|
fn = List.new([Sym.new('fn'), Vec.new([temp]), *body])
|
|
153
|
-
List.new([Sym.new('
|
|
153
|
+
List.new([Sym.new('.'), value, :tap, fn])
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
def gensym(prefix)
|
|
157
157
|
@gensym_index = (@gensym_index || 0) + 1
|
|
158
158
|
GeneratedSym.new("#{prefix}_#{@gensym_index}", @gensym_index)
|
|
159
159
|
end
|
|
160
|
+
|
|
161
|
+
def prepend_call_arg(form, value)
|
|
162
|
+
List.new([form.head, value, *form.rest])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def append_call_arg(form, value)
|
|
166
|
+
List.new([*form.items, value])
|
|
167
|
+
end
|
|
160
168
|
end
|
|
161
169
|
end
|
|
162
170
|
end
|
data/lib/kapusta/compiler.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'error'
|
|
4
4
|
require_relative 'compiler/lua_compat'
|
|
5
|
+
require_relative 'compiler/language'
|
|
5
6
|
require_relative 'compiler/normalizer'
|
|
6
7
|
require_relative 'compiler/emitter'
|
|
7
8
|
require_relative 'compiler/macro_expander'
|
|
@@ -9,30 +10,8 @@ require_relative 'compiler/macro_expander'
|
|
|
9
10
|
module Kapusta
|
|
10
11
|
module Compiler
|
|
11
12
|
class Error < Kapusta::Error; end
|
|
12
|
-
CORE_SPECIAL_FORMS =
|
|
13
|
-
|
|
14
|
-
while for each do values
|
|
15
|
-
-> ->> -?> -?>> doto
|
|
16
|
-
icollect collect fcollect accumulate faccumulate
|
|
17
|
-
hashfn
|
|
18
|
-
. ?. :
|
|
19
|
-
..
|
|
20
|
-
length
|
|
21
|
-
require
|
|
22
|
-
module class end
|
|
23
|
-
try catch finally
|
|
24
|
-
raise
|
|
25
|
-
ivar cvar gvar
|
|
26
|
-
ruby
|
|
27
|
-
tset
|
|
28
|
-
and or not
|
|
29
|
-
= not= < <= > >=
|
|
30
|
-
+ - * / %
|
|
31
|
-
print
|
|
32
|
-
macro macros import-macros
|
|
33
|
-
quasi-sym quasi-list quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym
|
|
34
|
-
].freeze
|
|
35
|
-
SPECIAL_FORMS = (CORE_SPECIAL_FORMS + LuaCompat::SPECIAL_FORMS).freeze
|
|
13
|
+
CORE_SPECIAL_FORMS = Language::CORE_SPECIAL_FORMS
|
|
14
|
+
SPECIAL_FORMS = Language::SPECIAL_FORMS
|
|
36
15
|
|
|
37
16
|
def self.compile(source, path: '(kapusta)', target: nil)
|
|
38
17
|
forms = Reader.read_all(source)
|
data/lib/kapusta/env.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Kapusta
|
|
4
4
|
class Env
|
|
5
|
-
MethodBinding = Struct.new(:ruby_name)
|
|
6
|
-
SelfMethodBinding = Struct.new(:ruby_name)
|
|
5
|
+
MethodBinding = Struct.new(:ruby_name, :multi_return)
|
|
6
|
+
SelfMethodBinding = Struct.new(:ruby_name, :multi_return)
|
|
7
7
|
|
|
8
8
|
def initialize(parent = nil)
|
|
9
9
|
@parent = parent
|
data/lib/kapusta/errors.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Kapusta
|
|
|
6
6
|
MESSAGES = {
|
|
7
7
|
accumulate_no_iterator: 'expected initial value and iterator binding table',
|
|
8
8
|
auto_gensym_outside_quasiquote: 'auto-gensym %{name}# outside quasiquote',
|
|
9
|
-
bad_multisym: 'bad multisym: %{path}',
|
|
9
|
+
bad_multisym: 'bad multisym: %{path}; unresolved root %{segment}; %{suggestion}',
|
|
10
10
|
bad_set_target: 'bad set target: %{target}',
|
|
11
11
|
bad_shorthand: 'bad shorthand',
|
|
12
12
|
bind_table_dots: 'unable to bind table ...',
|
|
@@ -42,6 +42,7 @@ module Kapusta
|
|
|
42
42
|
import_macros_module_no_exports: 'import-macros: module %{module} has no export table',
|
|
43
43
|
import_macros_module_not_found: 'import-macros: module %{module} not found',
|
|
44
44
|
invalid_class_name: 'invalid class name: %{name}',
|
|
45
|
+
invalid_header_body_form: '%{scope} body form must be a declaration or known special form: %{name}',
|
|
45
46
|
invalid_module_name: 'invalid module name: %{name}',
|
|
46
47
|
let_no_body: 'expected body expression',
|
|
47
48
|
let_odd_bindings: 'expected even number of name/value bindings',
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
class Formatter
|
|
5
|
+
module ASTHelpers
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def comment?(form)
|
|
9
|
+
form.is_a?(Comment)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def blank_line?(form)
|
|
13
|
+
form.is_a?(BlankLine)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def non_semantic?(form)
|
|
17
|
+
comment?(form) || blank_line?(form)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def contains_comments?(items)
|
|
21
|
+
items.any? { |item| non_semantic?(item) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def semantic_items(items)
|
|
25
|
+
items.reject { |item| non_semantic?(item) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def list_head(list)
|
|
29
|
+
semantic_items(list.items).first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def head_name(list)
|
|
33
|
+
head = list_head(list)
|
|
34
|
+
head.name if head.is_a?(Sym)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def list_rest(list)
|
|
38
|
+
semantic_items(list.items).drop(1)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def list_raw_rest(list)
|
|
42
|
+
index = list.items.index { |item| !non_semantic?(item) }
|
|
43
|
+
return list.items if index.nil?
|
|
44
|
+
|
|
45
|
+
list.items[(index + 1)..] || []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def split_raw_items(items, semantic_count)
|
|
49
|
+
split_index = 0
|
|
50
|
+
seen = 0
|
|
51
|
+
|
|
52
|
+
while split_index < items.length && seen < semantic_count
|
|
53
|
+
seen += 1 unless non_semantic?(items[split_index])
|
|
54
|
+
split_index += 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
[items.take(split_index), items.drop(split_index)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def multiline_in_source?(form)
|
|
61
|
+
form.respond_to?(:multiline_source) && form.multiline_source
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def contains_collection?(form)
|
|
65
|
+
case form
|
|
66
|
+
when List then semantic_items(form.items).any? { |item| collection?(item) }
|
|
67
|
+
when Vec then form.items.any? { |item| collection?(item) }
|
|
68
|
+
when HashLit then form.pairs.any? { |k, v| collection?(k) || collection?(v) }
|
|
69
|
+
else false
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def collection?(form)
|
|
74
|
+
form.is_a?(List) || form.is_a?(Vec) || form.is_a?(HashLit)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
class Formatter
|
|
5
|
+
module CLI
|
|
6
|
+
def initialize(argv)
|
|
7
|
+
@mode = :stdout
|
|
8
|
+
@files = []
|
|
9
|
+
@version = false
|
|
10
|
+
parse_args(argv)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
return print_version if @version
|
|
15
|
+
|
|
16
|
+
validate_args!
|
|
17
|
+
apply_mode(formatted_files)
|
|
18
|
+
rescue Kapusta::Error => e
|
|
19
|
+
warn e.formatted
|
|
20
|
+
1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def print_version
|
|
26
|
+
puts "kapfmt #{Kapusta::VERSION}"
|
|
27
|
+
0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def formatted_files
|
|
31
|
+
@files.map { |path| formatted_file(path) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def formatted_file(path)
|
|
35
|
+
original = read_source(path)
|
|
36
|
+
validate_kapusta_source(original, path)
|
|
37
|
+
[path, original, format_source(original, path)]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_mode(formatted)
|
|
41
|
+
case @mode
|
|
42
|
+
when :stdout
|
|
43
|
+
$stdout.write(formatted.first[2])
|
|
44
|
+
when :fix
|
|
45
|
+
fix_files(formatted)
|
|
46
|
+
when :check
|
|
47
|
+
return check_files(formatted)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fix_files(formatted)
|
|
54
|
+
formatted.each do |path, _original, rewritten|
|
|
55
|
+
raise Error, 'Cannot use --fix with stdin (-).' if stdin_path?(path)
|
|
56
|
+
|
|
57
|
+
File.write(path, rewritten)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check_files(formatted)
|
|
62
|
+
dirty = formatted.reject { |_path, original, rewritten| original == rewritten }
|
|
63
|
+
dirty.each do |path, _original, _rewritten|
|
|
64
|
+
warn "Not formatted: #{path}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
dirty.empty? ? 0 : 1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_args(argv)
|
|
71
|
+
argv.each do |arg|
|
|
72
|
+
case arg
|
|
73
|
+
when '--fix'
|
|
74
|
+
ensure_mode!(:fix)
|
|
75
|
+
when '--check'
|
|
76
|
+
ensure_mode!(:check)
|
|
77
|
+
when '--version', '-v'
|
|
78
|
+
@version = true
|
|
79
|
+
when '--help', '-h'
|
|
80
|
+
print_help
|
|
81
|
+
exit 0
|
|
82
|
+
else
|
|
83
|
+
@files << arg
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ensure_mode!(mode)
|
|
89
|
+
raise Error, 'Use at most one of --fix or --check.' if @mode != :stdout && @mode != mode
|
|
90
|
+
|
|
91
|
+
@mode = mode
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_args!
|
|
95
|
+
raise Error, 'Usage: kapfmt [--fix] [--check] FILENAME...' if @files.empty?
|
|
96
|
+
raise Error, 'stdin (-) may only be specified once.' if @files.count { |path| stdin_path?(path) } > 1
|
|
97
|
+
raise Error, 'Cannot use --fix with stdin (-).' if @mode == :fix && @files.any? { |path| stdin_path?(path) }
|
|
98
|
+
|
|
99
|
+
return unless @mode == :stdout && @files.length != 1
|
|
100
|
+
|
|
101
|
+
raise Error, 'Without --fix or --check, kapfmt accepts exactly one file.'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def read_source(path)
|
|
105
|
+
return File.read(path) unless stdin_path?(path)
|
|
106
|
+
|
|
107
|
+
@stdin_read ||= false
|
|
108
|
+
raise Error, 'stdin (-) may only be specified once.' if @stdin_read
|
|
109
|
+
|
|
110
|
+
@stdin_read = true
|
|
111
|
+
$stdin.read
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def stdin_path?(path)
|
|
115
|
+
path == STDIN_PATH
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def print_help
|
|
119
|
+
puts 'Usage: kapfmt [--fix] [--check] FILENAME...'
|
|
120
|
+
puts
|
|
121
|
+
puts 'Formats Kapusta source using the built-in Kapusta reader and pretty-printer.'
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
class Formatter
|
|
5
|
+
module LineHelpers
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def fits?(text, indent)
|
|
9
|
+
fits_within?(text, indent, MAX_WIDTH)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def inline_arg_fits?(text, indent)
|
|
13
|
+
fits_within?(text, indent, MAX_WIDTH - 1)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fits_within?(text, indent, width)
|
|
17
|
+
!text.include?("\n") && indent + text.length <= width
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def single_line?(text)
|
|
21
|
+
!text.include?("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def indent_block(text, amount)
|
|
25
|
+
prefix = ' ' * amount
|
|
26
|
+
text.lines.map { |line| line.strip.empty? ? blank_line_for(line) : "#{prefix}#{line}" }.join
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def blank_line_for(line)
|
|
30
|
+
line.end_with?("\n") ? "\n" : ''
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def append_suffix(lines, suffix)
|
|
34
|
+
updated = lines.dup
|
|
35
|
+
if updated[-1].lstrip.start_with?(';')
|
|
36
|
+
updated << suffix
|
|
37
|
+
else
|
|
38
|
+
updated[-1] = "#{updated[-1]}#{suffix}"
|
|
39
|
+
end
|
|
40
|
+
updated.join("\n")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
class Formatter
|
|
5
|
+
module Validator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def validate_kapusta_source(source, path)
|
|
9
|
+
return validate_macro_module_source(source, path) if macro_module_path?(path)
|
|
10
|
+
|
|
11
|
+
Kapusta::Compiler.compile(source, path:)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate_macro_module_source(source, path)
|
|
15
|
+
forms = Reader.read_all(source)
|
|
16
|
+
raise Error, 'macro module has no export table' unless forms.last.is_a?(HashLit)
|
|
17
|
+
|
|
18
|
+
processed = forms.map do |form|
|
|
19
|
+
Compiler::MacroLowerer.lower_module_form(form, error_class: Error)
|
|
20
|
+
end
|
|
21
|
+
wrapper = List.new([List.new([Sym.new('fn'), Vec.new([]), *processed])])
|
|
22
|
+
Compiler.compile_forms([wrapper], path:)
|
|
23
|
+
rescue Kapusta::Error => e
|
|
24
|
+
raise e.with_defaults(path:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def macro_module_path?(path)
|
|
28
|
+
path && File.extname(path) == '.kapm'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|