lat 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 051514055bc30ad3baf609b932c2b252f842ca9d864f929a6f8115ad72847de7
4
- data.tar.gz: 6352e1091aab7ba991cec765bd77941eba6d92ff998684b2958fd92ccce1e17a
3
+ metadata.gz: c2053d16f88f07ebebeb98b8132ae6bf6adfaecb06d1573be7c0639b73755837
4
+ data.tar.gz: 02063ec4e1819c2743e426bdbc4b642f4e0862b1ce4cdfd4b02e2aba69abc380
5
5
  SHA512:
6
- metadata.gz: d5c337be6a2c2d6525638fbcc0c3e57b2fcacc021501af218c2e5a452a4fb526356df6c5a5210cef99262f91f7c26c1d6cffdb0857caccc29f875547e15c5fa2
7
- data.tar.gz: adbc284ef685eb4ceac54abe6dfe7a4337e29ea99f5784050ac28a47aff13194c6f61a4a26089231ed100f2a4fcc49839a1fcc39b5b4efbc6b257c365aeed51f
6
+ metadata.gz: d080afc37148a4c9cab7daf30cdada85ab226f11a0bc13a7786ba3aa840156fc5b622ab5474dec0e7879926fa0952a09024a463afb93800b11facd0b15336a6c
7
+ data.tar.gz: 6674298934d89ce7f1f9c1e108e114a64d96330bb7d7e06ce27becd8e8e0f2b89cf2a71a5a0d963427c202ee2586a1cf5b13d824ef4f021168da04b6681f039e
data/compiler/codegen.rb CHANGED
@@ -1,15 +1,29 @@
1
1
  class Generator
2
+
3
+ def generate_statement(node)
4
+ code = generate(node)
5
+ line = node.instance_variable_get(:@lat_line) if node.respond_to?(:instance_variable_get)
6
+ line ? "--[[@#{line}]]#{code}" : code
7
+ end
8
+
9
+ def generate_arguments(args)
10
+ args.map { |expr| generate(expr) }.join(",")
11
+ end
12
+
13
+ def generate_param_names(names)
14
+ names.join(",")
15
+ end
16
+
2
17
  def generate(node)
3
18
  if node.is_a?(Array)
4
- return node.map { |n| generate(n) }.join("\n")
19
+ return node.map { |n| generate_statement(n) }.join("\n")
5
20
  end
6
21
 
7
22
  case node
8
23
 
9
24
  when ImportNode
10
- "require %s" % [
11
- node.location
12
- ]
25
+ "require(\"#{node.location}\")"
26
+
13
27
  when ClassNode
14
28
  out = []
15
29
  out << "local #{node.name} = {}"
@@ -40,20 +54,15 @@ class Generator
40
54
 
41
55
  out.join("\n")
42
56
  when ClassDefNode
43
- body_code = node.body.map { |n| generate(n) }.join("\n ")
44
- "function %s(%s)\n %s\nend" % [node.name, node.args.join(","), body_code]
57
+ body_code = generate(node.body)
58
+ "function %s(%s)\n %s\nend" % [node.name, generate_param_names(node.args), body_code]
45
59
 
46
60
  when DefNode
47
- body_code =
48
- if node.body.is_a?(Array)
49
- node.body.map { |n| generate (n) }.join("\n ")
50
- else
51
- generate(node.body)
52
- end
61
+ body_code = generate(node.body)
53
62
 
54
63
  "function #{node.type == "love"? "love." : ""}%s(%s)\n %s \nend" % [
55
64
  node.name,
56
- node.args.join(","),
65
+ generate_param_names(node.args),
57
66
  body_code
58
67
  ]
59
68
 
@@ -62,34 +71,60 @@ class Generator
62
71
 
63
72
  first = node.condition
64
73
  compiled << "if #{generate(first)} then\n "
65
- compiled << node.body.map { |n| generate (n) }.join("\n")
74
+ compiled << generate(node.body)
66
75
 
67
76
  node.elif_blocks.each do |c|
68
77
  compiled << "\nelseif #{generate(c.condition)} then\n "
69
- compiled << c.body.map { |n| generate (n) }.join("\n")
78
+ compiled << generate(c.body)
70
79
  end
71
80
 
72
81
  if node.else_body
73
82
  compiled << "\nelse\n "
74
- compiled << node.else_body.map { |n| generate (n) }.join("\n")
83
+ compiled << generate(node.else_body)
75
84
  end
76
85
 
77
86
  compiled << "\nend\n"
78
87
  compiled
79
88
 
80
89
  when WhileNode
81
- body_code =
82
- if node.body.is_a?(Array)
83
- node.body.map{ |n| generate (n) }.join("\n")
84
- else
85
- generate(node.body)
86
- end
90
+ body_code = generate(node.body)
87
91
 
88
92
  "while %s do \n %s \nend" % [
89
93
  generate(node.statement),
90
94
  body_code
91
95
  ]
92
96
 
97
+ when ForNode
98
+ body_code = generate(node.body)
99
+
100
+ increment = ", #{node.step.nil? ? "" : generate(node.step)}"
101
+ "for %s = %s, %s%s do\n %s \nend" % [
102
+ generate(node.var),
103
+ generate(node.start),
104
+ generate(node.stop),
105
+ increment,
106
+ body_code
107
+ ]
108
+
109
+ when ForPairNode
110
+ body_code = generate(node.body)
111
+ "for %s, %s in pairs(%s) do\n %s \nend" % [
112
+ node.key,
113
+ node.val,
114
+ generate(node.t),
115
+ body_code
116
+ ]
117
+
118
+ when ForIPairNode
119
+ body_code = generate(node.body)
120
+
121
+ "for %s, %s in ipairs(%s) do\n %s \nend" % [
122
+ node.index,
123
+ node.val,
124
+ generate(node.t),
125
+ body_code
126
+ ]
127
+
93
128
  when SwitchNode
94
129
  compiled = ""
95
130
  node.cases.each_with_index do |c, i|
@@ -99,7 +134,7 @@ class Generator
99
134
  compiled << "elseif #{generate(c.match)} == #{generate(node.value)} then\n"
100
135
  end
101
136
 
102
- body_code = c.body.map { |b| generate(b) }.join("\n")
137
+ body_code = generate(c.body)
103
138
  compiled << " #{body_code}\n"
104
139
 
105
140
  end
@@ -110,12 +145,12 @@ class Generator
110
145
  when CallNode
111
146
  "%s(%s)" % [
112
147
  node.name,
113
- node.arg_expr.map { |expr| generate(expr) }.join(",")
148
+ generate_arguments(node.arg_expr)
114
149
  ]
115
150
 
116
151
  when PrintNode
117
152
  "print(%s)" % [
118
- node.args.map { |expr| generate(expr) }.join(",")
153
+ generate_arguments(node.args)
119
154
  ]
120
155
 
121
156
  when VarAssignNode
@@ -126,7 +161,7 @@ class Generator
126
161
 
127
162
  when VarSetNode
128
163
  "%s = %s" % [
129
- node.name,
164
+ node.children.join("."),
130
165
  generate(node.value)
131
166
  ]
132
167
 
@@ -142,7 +177,7 @@ class Generator
142
177
  selfstring = "self#{node.type}#{node.name}"
143
178
 
144
179
  if !node.args.empty?
145
- args_string = node.args.map {|expr| generate(expr) }.join(",")
180
+ args_string = generate_arguments(node.args)
146
181
  selfstring += "(#{args_string})"
147
182
  else
148
183
  selfstring += " = %s" % [
@@ -155,7 +190,7 @@ class Generator
155
190
  "love.%s.%s(%s)" % [
156
191
  node.namespace,
157
192
  node.name,
158
- node.args.map { |expr| generate(expr) }.join(",")
193
+ generate_arguments(node.args)
159
194
  ]
160
195
 
161
196
  when ReturnNode
@@ -169,152 +204,21 @@ class Generator
169
204
  when IntegerNode
170
205
  node.value.to_s
171
206
 
207
+ when FloatNode
208
+ node.value.to_s
209
+
172
210
  when StringNode
173
211
  "\"%s\"" % [
174
212
  node.value
175
213
  ]
176
214
 
177
215
  when ArrayNode
178
- elements = node.elements.map { |e| generate(e) }.join(", ")
216
+ elements = generate_arguments(node.elements)
179
217
  "{#{elements}}"
180
218
 
181
219
  when ArrayAccessNode
182
220
  "#{node.name}[#{generate(node.index)}]"
183
221
 
184
- # when ErrorCallNode
185
- # <<~LUA
186
- # local utf8 = require("utf8")
187
-
188
- # local function error_printer(msg, layer)
189
- # print((debug.traceback("Error: " .. tostring(msg), 1+(layer or 1)):gsub("\n[^\n]+$", "")))
190
- # end
191
-
192
- # function love.errorhandler(msg)
193
- # msg = tostring(msg)
194
-
195
- # error_printer(msg, 2)
196
-
197
- # if not love.window or not love.graphics or not love.event then
198
- # return
199
- # end
200
-
201
- # if not love.graphics.isCreated() or not love.window.isOpen() then
202
- # local success, status = pcall(love.window.setMode, 800, 600)
203
- # if not success or not status then
204
- # return
205
- # end
206
- # end
207
-
208
- # -- Reset state.
209
- # if love.mouse then
210
- # love.mouse.setVisible(true)
211
- # love.mouse.setGrabbed(false)
212
- # love.mouse.setRelativeMode(false)
213
- # if love.mouse.isCursorSupported() then
214
- # love.mouse.setCursor()
215
- # end
216
- # end
217
- # if love.joystick then
218
- # -- Stop all joystick vibrations.
219
- # for i,v in ipairs(love.joystick.getJoysticks()) do
220
- # v:setVibration()
221
- # end
222
- # end
223
- # if love.audio then love.audio.stop() end
224
-
225
- # love.graphics.reset()
226
- # local font = love.graphics.setNewFont(14)
227
-
228
- # love.graphics.setColor(1, 1, 1)
229
-
230
- # local trace = debug.traceback()
231
-
232
- # love.graphics.origin()
233
-
234
- # local sanitizedmsg = {}
235
- # for char in msg:gmatch(utf8.charpattern) do
236
- # table.insert(sanitizedmsg, char)
237
- # end
238
- # sanitizedmsg = table.concat(sanitizedmsg)
239
-
240
- # local err = {}
241
-
242
- # table.insert(err, "Error\n")
243
- # table.insert(err, sanitizedmsg)
244
-
245
- # if #sanitizedmsg ~= #msg then
246
- # table.insert(err, "Invalid UTF-8 string in error message.")
247
- # end
248
-
249
- # table.insert(err, "\n")
250
-
251
- # for l in trace:gmatch("(.-)\n") do
252
- # if not l:match("boot.lua") then
253
- # l = l:gsub("stack traceback:", "Traceback\n")
254
- # table.insert(err, l)
255
- # end
256
- # end
257
-
258
- # local p = table.concat(err, "\n")
259
-
260
- # p = p:gsub("\t", "")
261
- # p = p:gsub("%[string \"(.-)\"%]", "%1")
262
-
263
- # local function draw()
264
- # if not love.graphics.isActive() then return end
265
- # local pos = 70
266
- # love.graphics.clear(89/255, 157/255, 220/255)
267
- # love.graphics.printf(p, pos, pos, love.graphics.getWidth() - pos)
268
- # love.graphics.present()
269
- # end
270
-
271
- # local fullErrorText = p
272
- # local function copyToClipboard()
273
- # if not love.system then return end
274
- # love.system.setClipboardText(fullErrorText)
275
- # p = p .. "\nCopied to clipboard!"
276
- # end
277
-
278
- # if love.system then
279
- # p = p .. "\n\nPress Ctrl+C or tap to copy this error"
280
- # end
281
-
282
- # return function()
283
- # love.event.pump()
284
-
285
- # for e, a, b, c in love.event.poll() do
286
- # if e == "quit" then
287
- # return 1
288
- # elseif e == "keypressed" and a == "escape" then
289
- # return 1
290
- # elseif e == "keypressed" and a == "c" and love.keyboard.isDown("lctrl", "rctrl") then
291
- # copyToClipboard()
292
- # elseif e == "touchpressed" then
293
- # local name = love.window.getTitle()
294
- # if #name == 0 or name == "Untitled" then name = "Game" end
295
- # local buttons = {"OK", "Cancel"}
296
- # if love.system then
297
- # buttons[3] = "Copy to clipboard"
298
- # end
299
- # local pressed = love.window.showMessageBox("Quit "..name.."?", "", buttons)
300
- # if pressed == 1 then
301
- # return 1
302
- # elseif pressed == 3 then
303
- # copyToClipboard()
304
- # end
305
- # end
306
- # end
307
-
308
- # draw()
309
-
310
- # if love.timer then
311
- # love.timer.sleep(0.1)
312
- # end
313
- # end
314
-
315
- # end
316
- # LUA
317
-
318
222
  else
319
223
  raise "Unknown node type: #{node.class}"
320
224
  end
data/compiler/compile.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "tokenizer"
4
4
  require_relative "parser"
5
5
  require_relative "codegen"
6
+ require_relative "errors"
6
7
 
7
8
  if ARGV.empty?
8
9
  puts "Usage: lat <input.lat> [output.lua]"
@@ -19,8 +20,26 @@ def detect_os
19
20
  :linux
20
21
  end
21
22
 
23
+ OS = detect_os
24
+
25
+ def report_syntax_error(err, filename, source)
26
+ location = err.line ? "#{filename}:#{err.line}" : filename
27
+ puts "lat: syntax error in #{location}"
28
+
29
+ if err.line && source
30
+ src_line = source.split("\n", -1)[err.line - 1]
31
+ if src_line
32
+ puts " #{src_line}"
33
+ puts " #{" " * (err.column - 1)}^" if err.column
34
+ end
35
+ end
36
+
37
+ puts " #{err.message}"
38
+ end
39
+
40
+
22
41
  def love_installed?
23
- if detect_os == :windows
42
+ if OS == :windows
24
43
  common_paths = [
25
44
  "C:/Program Files/LOVE/love.exe",
26
45
  "C:/Program Files (x86)/LOVE/love.exe",
@@ -33,15 +52,14 @@ def love_installed?
33
52
  end
34
53
 
35
54
  def install_love
36
- os = detect_os
37
55
  puts "[lat] Love2D not found, attempting to install"
38
- print "Do you want to install Love2d? (y/n)"
56
+ print "Do you want to install LOVE2D? (y/n)"
39
57
  answer = $stdin.gets.chomp.downcase
40
58
 
41
59
  if %w[y yes].include?(answer.downcase)
42
60
  puts "installing love..."
43
61
 
44
- case os
62
+ case OS
45
63
  when :arch
46
64
  system("sudo pacman -S --noconfirm love")
47
65
  when :debian
@@ -52,11 +70,11 @@ def install_love
52
70
  if system("which brew > /dev/null 2>&1")
53
71
  system("brew install love")
54
72
  else
55
- puts "[lat] Homebrew not found. Install Love2D from https://love2d.org"
73
+ puts "[lat] Homebrew not found. Install LOVE2D from https://love2d.org"
56
74
  exit 1
57
75
  end
58
76
  when :windows
59
- puts "[lat] Please install Love2D from https://love2d.org"
77
+ puts "[lat] Please install LOVE2D from https://love2d.org"
60
78
  puts "[lat] Set LOVE_PATH to the love.exe location:"
61
79
  puts '[lat] setx LOVE_PATH "C:\\Program Files\\LOVE\\love.exe"'
62
80
  else
@@ -74,22 +92,19 @@ end
74
92
  def find_love
75
93
  install_love unless love_installed?
76
94
 
77
- candidates = [
78
- "love",
79
- "C:/Program Files/LOVE/love.exe",
80
- "C:/Program Files (x86)/LOVE/love.exe",
81
- ENV["LOVE_PATH"]
82
- ].compact
83
-
84
- candidates.each do |path|
85
- return path if system("where #{path} >nul 2>&1") || File.exist?(path.to_s)
95
+ if OS == :windows
96
+ candidates = [ENV["LOVE_PATH"], "C:/Program Files/LOVE/love.exe", "C:/Program Files (x86)/LOVE/love.exe"].compact
97
+ candidates.each { |p| return p if File.exist?(p) }
98
+ else
99
+ ["love", "love2d"].each do |cmd|
100
+ return cmd if system("which #{cmd} > /dev/null 2>&1")
101
+ end
86
102
  end
87
103
 
88
104
  puts "Error: LOVE2D not found. Install it from https://love2d.org or set LOVE_PATH env variable."
89
105
  exit 1
90
106
  end
91
107
  skip_run = false
92
- love = find_love()
93
108
 
94
109
  if ARGV[0] == "run"
95
110
  latcDir = File.join(Dir.pwd, ".latc")
@@ -102,6 +117,7 @@ if ARGV[0] == "run"
102
117
  puts "Error: .latc exists but no main.lua found"
103
118
  exit 1
104
119
  end
120
+ love = find_love()
105
121
  exec(love, latcDir)
106
122
  elsif ARGV[0] == "build"
107
123
  ARGV.shift
@@ -110,7 +126,7 @@ end
110
126
  inputFile = ARGV[0] || "main.lat"
111
127
 
112
128
  unless File.exist?(inputFile)
113
- puts "Error: fil `#{inputFile}` not found"
129
+ puts "Error: file `#{inputFile}` not found"
114
130
  exit 1
115
131
  end
116
132
 
@@ -121,24 +137,114 @@ if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
121
137
  system("attrib +h #{latcDir}") # -h to unhide
122
138
  end
123
139
 
140
+ inputDir = File.dirname(inputFile)
124
141
  basename = File.basename(inputFile, ".*")
125
142
  outputFile = File.join(latcDir, basename == "main" ? "main.lua" : "#{basename}.lua")
143
+ confOutputFile = File.join(latcDir, "conf.lua")
144
+
145
+ confPath = File.join(inputDir, "conf.lat")
146
+ confInput = File.read(confPath) if File.exist?(confPath)
126
147
 
127
148
  input = File.read(inputFile)
149
+ confInput = File.read(confPath) if File.exist?(confPath)
128
150
 
129
- tokens = Tokenizer.new(input).tokenize
151
+ confTokens = Tokenizer.new(confInput).tokenize if confInput
130
152
  # puts "--- TOKENS ---"
131
153
  # puts tokens.map(&:inspect).join("\n")
132
154
 
133
- tree = Parser.new(tokens).parse
155
+ confTree = Parser.new(confTokens).parse if confTokens
134
156
  # puts "--- AST ---"
135
157
  # p tree
136
158
 
159
+
160
+ begin
161
+ tokens = Tokenizer.new(input).tokenize
162
+ tree = Parser.new(tokens).parse
163
+ rescue LatSyntaxError => e
164
+ report_syntax_error(e, inputFile, input)
165
+ exit 1
166
+ end
167
+
168
+ confTree = nil
169
+ if confInput
170
+ begin
171
+ confTokens = Tokenizer.new(confInput).tokenize
172
+ confTree = Parser.new(confTokens).parse
173
+ rescue LatSyntaxError => e
174
+ report_syntax_error(e, confPath, confInput)
175
+ exit 1
176
+ end
177
+ end
178
+
179
+
180
+
181
+
137
182
  generated = Generator.new.generate(tree)
183
+ confGenerated = Generator.new.generate(confTree) if confTree
138
184
  # puts "--- LUA OUTPUT ---"
139
185
  # puts generated
140
- File.open(outputFile, 'w') { |file| file.write(generated)}
141
186
 
142
- exec(love, latcDir) unless skip_run
187
+ def build_source_map(lua_source)
188
+ map = {}
189
+ clean_lines = lua_source.split("\n", -1).each_with_index.map do |line, idx|
190
+ if line =~ /\A(\s*)--\[\[@(\d+)\]\](.*)\z/m
191
+ map[idx + 1] = $2.to_i
192
+ "#{$1}#{$3}"
193
+ else
194
+ line
195
+ end
196
+ end
197
+ [clean_lines.join("\n"), map]
198
+ end
199
+
200
+ def build_lat_shim(basename, source_map)
201
+ table_entries = source_map.map { |k, v| "[#{k}]=#{v}" }.join(",")
202
+ <<~LUA
203
+ local _LAT_SOURCE_MAP_T = { #{table_entries} }
204
+ local _LAT_FILE = "#{basename}.lat"
205
+
206
+ local function _lat_remap(text)
207
+ return (text:gsub("#{basename}%.lua:(%d+)", function(n)
208
+ local mapped = _LAT_SOURCE_MAP_T[tonumber(n)]
209
+ return mapped and (_LAT_FILE .. ":" .. mapped) or ("#{basename}.lua:" .. n)
210
+ end))
211
+ end
212
+
213
+ local _LAT_ERROR_BG = {0.1, 0.1, 0.15}
214
+
215
+ if _lat_default_handler then
216
+ local function _lat_wrapped_handler(msg)
217
+ local loop = _lat_default_handler(_lat_remap(tostring(msg)))
218
+ if type(loop) == "function" then
219
+ local real_clear = love.graphics.clear
220
+ return function(...)
221
+ love.graphics.clear = function() real_clear(_LAT_ERROR_BG[1], _LAT_ERROR_BG[2], _LAT_ERROR_BG[3]) end
222
+ local result = loop(...)
223
+ love.graphics.clear = real_clear
224
+ return result
225
+ end
226
+ end
227
+ return loop
228
+ end
229
+ love.errorhandler = _lat_wrapped_handler
230
+ love.errhand = _lat_wrapped_handler
231
+ end
232
+
233
+ LUA
234
+ end
235
+
236
+
237
+ generated, source_map = build_source_map(generated)
238
+
239
+ shim_line_count = build_lat_shim(basename, {}).each_line.count
240
+ offset_map = source_map.each_with_object({}) { |(k, v), h| h[k + shim_line_count] = v }
241
+
242
+ generated = build_lat_shim(basename, offset_map) + generated
243
+
244
+ confGenerated, _conf_source_map = build_source_map(confGenerated) if confGenerated
245
+
246
+ File.open(outputFile, 'w') { |file| file.write(generated) }
247
+ File.open(confOutputFile, 'w') { |file| file.write(confGenerated) } if confGenerated
248
+ exec(find_love(), latcDir) unless skip_run
143
249
 
144
250
  # ln -s "$(pwd)/compiler/compile.rb" /usr/local/bin/lat
@@ -0,0 +1,8 @@
1
+ class LatSyntaxError < StandardError
2
+ attr_reader :line, :column
3
+ def initialize(message, line: nil, column: nil)
4
+ @line = line
5
+ @column = column
6
+ super(message)
7
+ end
8
+ end
data/compiler/parser.rb CHANGED
@@ -6,13 +6,20 @@ IfNode = Struct.new(:condition, :body, :elif_blocks, :else_body)
6
6
  ElifBlock = Struct.new(:condition, :body)
7
7
 
8
8
  ImportNode = Struct.new(:location)
9
+
9
10
  WhileNode = Struct.new(:statement, :body)
11
+ ForNode = Struct.new(:var, :start, :stop, :step, :body)
12
+ ForPairNode = Struct.new(:key, :val, :t, :body)
13
+ ForIPairNode = Struct.new(:index, :val, :t, :body)
14
+
10
15
  IntegerNode = Struct.new(:value)
16
+ FloatNode = Struct.new(:value)
11
17
  StringNode = Struct.new(:value)
18
+
12
19
  CallNode = Struct.new(:name, :arg_expr)
13
20
  VarRefNode = Struct.new(:value)
14
21
  VarAssignNode = Struct.new(:name, :value)
15
- VarSetNode = Struct.new(:name, :value)
22
+ VarSetNode = Struct.new(:children, :value)
16
23
  BinOpNode = Struct.new(:left, :op, :right)
17
24
  PrintNode = Struct.new(:args)
18
25
  ReturnNode = Struct.new(:statement)
@@ -28,7 +35,6 @@ SelfNode = Struct.new(:name, :type, :args, :value)
28
35
 
29
36
  ErrorCallNode = Struct.new()
30
37
 
31
-
32
38
  LOVE_NAMESPACES = {
33
39
  lgraphics: "graphics",
34
40
  laudio: "audio",
@@ -52,9 +58,48 @@ OP_NAMESPACES = {
52
58
 
53
59
  }
54
60
 
61
+ FRIENDLY_TOKEN_NAMES = {
62
+ end: "'done'",
63
+ def: "'call'",
64
+ local: "'nat'",
65
+ while: "'when'",
66
+ forpairs: "'each'",
67
+ foripairs: "'seq'",
68
+ in: "'as'",
69
+ import: "'bring'",
70
+ if: "'if'",
71
+ elif: "'elif'",
72
+ else: "'else'",
73
+ switch: "'switch'",
74
+ to: "'to'",
75
+ class: "'class'",
76
+ self: "'self'",
77
+ identifier: "an identifier",
78
+ string: "a string",
79
+ integer: "a number",
80
+ float: "a number",
81
+ oparen: "'('",
82
+ cparen: "')'",
83
+ lbracket: "'['",
84
+ rbracket: "']'",
85
+ lbrace: "'{'",
86
+ rbrace: "'}'",
87
+ comma: "','",
88
+ dot: "'.'",
89
+ colon: "':'",
90
+ equal: "'='",
91
+ dequal: "'=='",
92
+ newline: "a newline",
93
+ }.freeze
94
+
55
95
  class Parser
56
96
  def initialize(tokens)
57
97
  @tokens = tokens
98
+ @last_line = tokens.first&.line
99
+ end
100
+
101
+ def describe_token_type(type)
102
+ FRIENDLY_TOKEN_NAMES[type] || "'#{type}'"
58
103
  end
59
104
 
60
105
  def parse
@@ -79,30 +124,41 @@ class Parser
79
124
  skip_newlines
80
125
  return nil if peek(:end)
81
126
 
82
- if peek(:import) then
83
- parse_import
84
- elsif peek(:class) then
85
- parse_class
86
- elsif peek(:def)
87
- parse_def
88
- elsif peek(:if)
89
- parse_if
90
- elsif peek(:while)
91
- parse_while
92
- elsif peek(:switch)
93
- parse_switch
94
- elsif peek(:print)
95
- parse_print
96
- elsif peek(:local)
97
- parse_var_assign
98
- elsif peek(:identifier) && peek(:equal, 1)
99
- parse_var_set
100
- elsif peek(:return)
101
- parse_return
102
- else
103
- parse_expr
104
- end
127
+ line = @tokens[0]&.line
128
+
129
+ node =
130
+ if peek(:import) then
131
+ parse_import
132
+ elsif peek(:class) then
133
+ parse_class
134
+ elsif peek(:def)
135
+ parse_def
136
+ elsif peek(:if)
137
+ parse_if
138
+ elsif peek(:while)
139
+ parse_while
140
+ elsif peek(:for)
141
+ parse_for
142
+ elsif peek(:forpairs)
143
+ parse_forpairs
144
+ elsif peek(:foripairs)
145
+ parse_foripairs
146
+ elsif peek(:switch)
147
+ parse_switch
148
+ elsif peek(:print)
149
+ parse_print
150
+ elsif peek(:local)
151
+ parse_var_assign
152
+ elsif seems_var_set?
153
+ parse_var_set
154
+ elsif peek(:return)
155
+ parse_return
156
+ else
157
+ parse_expr
158
+ end
105
159
 
160
+ node.instance_variable_set(:@lat_line, line) if node.respond_to?(:instance_variable_set) && line
161
+ node
106
162
 
107
163
  end
108
164
 
@@ -110,6 +166,16 @@ class Parser
110
166
  consume(:newline) while peek(:newline)
111
167
  end
112
168
 
169
+ def seems_var_set?
170
+ return false unless peek(:identifier)
171
+ offset = 1
172
+
173
+ while peek(:dot, offset) && peek(:identifier, offset + 1)
174
+ offset += 2
175
+ end
176
+ peek(:equal, offset)
177
+ end
178
+
113
179
  def parse_import
114
180
  consume(:import)
115
181
  location = consume(:string).value
@@ -142,14 +208,8 @@ class Parser
142
208
  consume(:def)
143
209
  name = "#{name}:#{consume(:identifier).value}"
144
210
  args = parse_args
145
- skip_newlines
211
+ body = parse_block
146
212
 
147
- body = []
148
-
149
- until peek(:end)
150
- body << parse_statement
151
- skip_newlines
152
- end
153
213
  consume(:end)
154
214
  ClassDefNode.new(name, args, body)
155
215
  end
@@ -165,18 +225,11 @@ class Parser
165
225
  name = consume(:identifier).value
166
226
  args = parse_args
167
227
 
168
- skip_newlines
169
- body = []
170
-
171
- until peek(:end)
172
- body << parse_statement
173
- skip_newlines
174
- end
228
+ body = parse_block
175
229
  consume(:end)
176
230
  DefNode.new(type, name, args, body)
177
231
  end
178
232
 
179
-
180
233
  def parse_if
181
234
  consume(:if)
182
235
  condition = parse_expr
@@ -207,11 +260,8 @@ class Parser
207
260
  consume(:else)
208
261
  skip_newlines
209
262
 
210
- else_body = []
211
- while !peek(:end)
212
- else_body << parse_statement
213
- skip_newlines
214
- end
263
+ else_body = parse_block
264
+
215
265
  end
216
266
 
217
267
  consume(:end)
@@ -230,16 +280,56 @@ class Parser
230
280
  statement = parse_expr
231
281
  end
232
282
 
233
- skip_newlines
234
- body = []
283
+ body = parse_block
235
284
 
236
- until peek(:end)
237
- body << parse_statement
238
- skip_newlines
285
+ consume(:end)
286
+ WhileNode.new(statement, body)
287
+ end
288
+
289
+ def parse_for
290
+ consume(:for)
291
+ var = parse_expr
292
+ consume(:equal)
293
+ start = parse_expr
294
+ consume(:comma)
295
+ stop = parse_expr
296
+
297
+ step = nil
298
+ if peek(:comma)
299
+ consume(:comma)
300
+ step = parse_expr
239
301
  end
302
+ body = parse_block
240
303
 
241
304
  consume(:end)
242
- WhileNode.new(statement, body)
305
+ ForNode.new(var, start, stop, step, body)
306
+
307
+ end
308
+
309
+ def parse_forpairs
310
+ consume(:forpairs)
311
+ key = consume(:identifier).value
312
+ consume(:comma)
313
+ val = consume(:identifier).value
314
+ consume(:in)
315
+ t = parse_expr
316
+ body = parse_block
317
+ consume(:end)
318
+
319
+ ForPairNode.new(key, val, t, body)
320
+ end
321
+
322
+ def parse_foripairs
323
+ consume(:foripairs)
324
+ index = consume(:identifier).value
325
+ consume(:comma)
326
+ val = consume(:identifier).value
327
+ consume(:in)
328
+ t = parse_expr
329
+ body = parse_block
330
+ consume(:end)
331
+
332
+ ForIPairNode.new(index, val, t, body)
243
333
  end
244
334
 
245
335
  def parse_switch
@@ -283,10 +373,14 @@ class Parser
283
373
  end
284
374
 
285
375
  def parse_var_set
286
- name = consume(:identifier).value
376
+ children = [consume(:identifier).value] # First get the identifier
377
+ while peek(:dot) # Check if there is a dot or a child for it
378
+ consume(:dot)
379
+ children << consume(:identifier).value
380
+ end
287
381
  consume(:equal)
288
382
  value = parse_expr
289
- VarSetNode.new(name, value)
383
+ VarSetNode.new(children, value)
290
384
  end
291
385
 
292
386
  def parse_return
@@ -306,24 +400,6 @@ class Parser
306
400
 
307
401
  left
308
402
  end
309
- # consume(:if)
310
-
311
- # if peek(:oparen)
312
- # consume(:oparen)
313
- # statement = parse_expr
314
- # consume(:cparen)
315
- # else
316
- # statement = parse_expr
317
- # end
318
- # skip_newlines
319
- # body = []
320
-
321
- # until peek(:elif) peek(:end)
322
- # body << parse_statement
323
- # skip_newlines
324
- # end
325
- # consume(:end)
326
- # IfNode.new(statement, body)
327
403
 
328
404
  def parse_args
329
405
  consume(:oparen)
@@ -339,7 +415,16 @@ class Parser
339
415
  args
340
416
  end
341
417
 
418
+ def parse_block
419
+ skip_newlines
420
+ body = []
342
421
 
422
+ until peek(:end)
423
+ body << parse_statement
424
+ skip_newlines
425
+ end
426
+ body
427
+ end
343
428
 
344
429
  def parse_additive
345
430
  left = parse_multiplicative
@@ -362,41 +447,30 @@ class Parser
362
447
  end
363
448
  left
364
449
  end
365
- # def parse_operators
366
- # left = parse_term
367
-
368
- # left = parse_op(left, :divide)
369
- # left = parse_op(left, :multiply)
370
- # left = parse_op(left, :plus)
371
- # left = parse_op(left, :minus)
372
-
373
- # left
374
- # end
375
-
376
- # def parse_op(left, operator)
377
-
378
- # while peek(operator)
379
- # consume(operator)
380
- # right = parse_term
381
- # left = BinOpNode.new(left, operator, right)
382
- # end
383
- # left
384
-
385
- # end
386
450
 
387
451
  def parse_term
452
+ skip_newlines
388
453
  if peek(:integer)
389
454
  IntegerNode.new(consume(:integer).value.to_i)
390
455
 
456
+ elsif peek(:float)
457
+ FloatNode.new(consume(:float).value.to_f)
458
+
391
459
  elsif peek(:identifier) && peek(:oparen, 1)
392
460
  parse_call
393
461
 
462
+ elsif peek(:identifier) && peek(:lbracket, 1)
463
+ parse_array_access
464
+
465
+ elsif peek(:lbrace)
466
+ parse_array
467
+
394
468
  elsif peek(:identifier)
395
469
  VarRefNode.new(consume(:identifier).value)
396
470
 
397
471
  elsif peek(:string)
398
472
  strVal = consume(:string).value
399
- StringNode.new(strVal[1..-2])
473
+ StringNode.new(strVal[1..-2])
400
474
 
401
475
  elsif peek(:oparen)
402
476
  # consume(:oparen)
@@ -407,15 +481,19 @@ class Parser
407
481
 
408
482
  first = parse_expr
409
483
  items = [first]
410
- until peek(:identifier) && peek(:or) && peek(:dequal) && peek(:cparen) && peek(:plus)
484
+ until peek(:identifier) || peek(:dequal)
411
485
  break
412
486
  end
413
487
 
414
- if !peek(:identifier) && peek(:or)
488
+ if !peek(:identifier) && peek(:or) || peek(:and)
415
489
  while peek(:or)
416
490
  consume(:or)
417
491
  items << parse_expr
418
492
  end
493
+ while peek(:and)
494
+ consume(:and)
495
+ items << parse_expr
496
+ end
419
497
  consume(:cparen)
420
498
  return AndOrListNode.new(items)
421
499
  end
@@ -426,14 +504,27 @@ class Parser
426
504
  elsif peek(:self)
427
505
  parse_self_node
428
506
 
429
- elsif peek(:coparen)
430
- parse_array
507
+
431
508
 
432
509
 
433
510
  elsif LOVE_NAMESPACES.keys.include?(peek_type)
434
511
  parse_love_call
435
512
  else
436
- raise "Unexpected token #{@tokens[0].inspect} in term"
513
+ token = @tokens[0]
514
+ if token.nil?
515
+ raise LatSyntaxError.new(
516
+ "Expected an expression but reached the end of the file",
517
+ line: @last_line
518
+ )
519
+ end
520
+
521
+ raise LatSyntaxError.new(
522
+ "Expected an expression but got #{describe_token_type(token.type)} (\"#{token.value}\")",
523
+ line: token.line,
524
+ column: token.column
525
+
526
+ )
527
+ # raise "Unexpected token #{@tokens[0].inspect} in term"
437
528
 
438
529
  end
439
530
  end
@@ -444,7 +535,8 @@ class Parser
444
535
  prefix = @tokens.shift.type
445
536
  namespace = LOVE_NAMESPACES[prefix]
446
537
 
447
- name = consume(:identifier).value
538
+ name = @tokens.shift.value
539
+
448
540
  args = parse_arg_expr
449
541
 
450
542
  LoveCallNode.new(namespace, name, args)
@@ -499,32 +591,57 @@ class Parser
499
591
  end
500
592
 
501
593
  def parse_array
502
- consume(:coparen)
594
+ consume(:lbrace)
595
+ skip_newlines
503
596
  elements = []
504
- unless peek(:ccparen)
597
+ unless peek(:rbrace)
505
598
  elements << parse_expr
599
+ skip_newlines
506
600
  while peek(:comma)
507
601
  consume(:comma)
602
+ skip_newlines
508
603
  elements << parse_expr
604
+ skip_newlines
509
605
  end
510
606
  end
511
- consume(:ccparen)
607
+ skip_newlines
608
+ consume(:rbrace)
512
609
  ArrayNode.new(elements)
513
610
  end
514
611
 
515
612
 
516
613
  def parse_array_access
517
614
  name = consume(:identifier).value
518
- consume(:soparen)
615
+ consume(:lbracket)
519
616
  index = parse_expr
520
- consume(:scparen)
617
+ consume(:rbracket)
521
618
  ArrayAccessNode.new(name, index)
522
619
  end
523
620
 
524
621
  def consume(type)
525
622
  token = @tokens.shift
526
- raise "Expected #{type}, got #{token.type}" unless token.type == type
623
+ # raise "Expected #{type}, got #{token.type}" unless token.type == type
624
+ # token
625
+
626
+ if token.nil?
627
+ raise LatSyntaxError.new(
628
+ "Expected #{describe_token_type(type)} but reached the end of the file",
629
+ line: @last_line
630
+ )
631
+ end
632
+
633
+ unless token.type == type
634
+ raise LatSyntaxError.new(
635
+ "Expected #{describe_token_type(type)} but got #{describe_token_type(token.type)} (\"#{token.value}\")",
636
+ line: token.line,
637
+ column: token.column
638
+
639
+ )
640
+ end
641
+
642
+ @last_line = token.line
527
643
  token
644
+
528
645
  end
529
646
 
530
647
  def peek(type, offset = 0)
@@ -534,4 +651,4 @@ class Parser
534
651
  def peek_type(offset = 0)
535
652
  @tokens[offset]&.type
536
653
  end
537
- end
654
+ end
@@ -1,58 +1,68 @@
1
- Token = Struct.new(:type, :value)
1
+ Token = Struct.new(:type, :value, :line, :column)
2
2
 
3
3
  class Tokenizer
4
4
 
5
5
  TOKEN_TYPES = [
6
+
7
+ #love
8
+ [:lgraphics, /-G:/],
9
+ [:laudio, /-A:/],
10
+ [:ldata, /-D:/],
11
+ [:levent, /-E:/],
12
+ [:lfilesystem, /-FS:/],
13
+ [:lfont, /-F:/],
14
+ [:limage, /-I:/],
15
+ [:ljoystick, /-J:/],
16
+ [:lmouse, /-M:/],
17
+ [:lkeyboard, /-K:/],
18
+ [:love, /\blove\b/],
19
+
6
20
  [:local, /\bnat\b/],
7
21
  [:def, /\bcall\b/],
8
22
  [:end, /\bdone\b/],
9
23
  [:if, /\bif\b/],
10
24
  [:elif, /\belif\b/],
11
25
  [:else, /\belse\b/],
26
+
12
27
  [:while, /\bwhen\b/],
28
+ [:for, /\bfor\b/],
29
+ [:forpairs, /\beach\b/],
30
+ [:foripairs, /\bseq\b/],
31
+
13
32
  [:print, /\bprint\b/],
14
33
  [:return, /\breturn\b/],
15
34
  [:or, /\bor\b/],
16
35
  [:and, /\band\b/],
36
+ [:in, /\bas\b/],
17
37
  [:switch, /\bswitch\b/],
18
38
  [:to, /\bto\b/],
19
39
  [:class, /\bclass\b/],
20
40
  [:self, /\bself\b/],
21
41
  [:import, /\bbring\b/],
22
42
 
23
- #love
24
- [:lgraphics, /-G:/],
25
- [:laudio, /-A:/],
26
- [:ldata, /-D:/],
27
- [:levent, /-E:/],
28
- [:lfilesystem, /-FS:/],
29
- [:lfont, /-F:/],
30
- [:limage, /-I:/],
31
- [:ljoystick, /-J:/],
32
- [:lmouse, /-M:/],
33
- [:lkeyboard, /-K:/],
34
- [:love, /\blove\b/],
43
+
35
44
 
36
45
  # thingies
37
- [:identifier, /\b[a-zA-Z]+\b/],
46
+ [:identifier, /\b[a-zA-Z_][a-zA-Z0-9_]*\b/],
38
47
  [:string, /"([^"]*)"/],
39
- [:integer, /\b[0-9]+\b/],
48
+ [:float, /(?:\d+\.\d*|\.\d+)/],
49
+ [:integer, /\b\d+\b/],
40
50
  [:oparen, /\(/],
41
51
  [:cparen, /\)/],
42
- [:soparen, /\[/],
43
- [:scparen, /\]/],
44
- [:coparen, /\{/],
45
- [:ccparen, /\}/],
52
+ [:lbracket, /\[/],
53
+ [:rbracket, /\]/],
54
+ [:lbrace, /\{/],
55
+ [:rbrace, /\}/],
46
56
  [:comma, /,/],
47
57
  [:dot, /\./],
48
58
  [:colon, /:/],
49
59
 
50
60
  # operators
51
61
  [:dequal, /==/],
52
- [:greater, />/],
53
- [:lesser, /</],
54
62
  [:grequal, />=/],
55
63
  [:lequal, /<=/],
64
+ [:greater, />/],
65
+ [:lesser, /</],
56
66
  [:equal, /=/],
57
67
  [:divide, /\//],
58
68
  [:multiply, /\*/],
@@ -63,8 +73,12 @@ class Tokenizer
63
73
 
64
74
  ]
65
75
 
66
- def initialize(code)
67
- @code = code
76
+ def initialize(code, filename = nil)
77
+ @code = code.gsub(/\r\n?/, "\n")
78
+ @filename = filename
79
+ @line = 1
80
+ @column = 1
81
+ # puts "Tokenizer initialized with: #{code.inspect}"
68
82
  end
69
83
 
70
84
  def tokenize
@@ -79,19 +93,39 @@ class Tokenizer
79
93
  def tokenize_token
80
94
  TOKEN_TYPES.each do |type, regex|
81
95
  anchored = /\A(#{regex})/
82
-
83
-
96
+
84
97
  if @code =~ anchored
85
98
  value = $1
86
99
  if type == :space
87
100
  @code = @code[value.length..-1]
101
+ @column += value.length
88
102
  return tokenize_token
89
103
  end
104
+
105
+ token = Token.new(type, value, @line)
90
106
  @code = @code[value.length..-1]
91
- return Token.new(type, value)
107
+
108
+ if type == :newline
109
+ @line += value.count("\n")
110
+ @column = 1
111
+ else
112
+ @column += value.length
113
+ end
114
+
115
+ # return Token.new(type, value)
116
+ return token
92
117
  end
93
118
  end
94
119
 
95
- raise "Couldn't match token on #{@code.inspect}"
120
+ snippet = @code.lines.first.to_s.chomp
121
+ snippet = snippet[0, 30] + "..." if snippet.length > 30
122
+
123
+ raise LatSyntaxError.new(
124
+ "Unrecognized syntax near #{snippet.inspect}",
125
+ line: @line,
126
+ column: @column
127
+ )
128
+
129
+ # raise "Couldn't match token on #{@code.inspect}"
96
130
  end
97
131
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - JakeOJeff
@@ -17,6 +17,7 @@ files:
17
17
  - bin/lat
18
18
  - compiler/codegen.rb
19
19
  - compiler/compile.rb
20
+ - compiler/errors.rb
20
21
  - compiler/parser.rb
21
22
  - compiler/tokenizer.rb
22
23
  homepage: https://github.com/JakeOJeff/lat