Browse files

C and Lua line tracking starting to work well

  • Loading branch information...
1 parent cac4f07 commit 67cfc551bf70c99ae0be10a54895f08f7872441f @stevedonovan committed Jun 8, 2011
Showing with 184 additions and 29 deletions.
  1. +77 −17 luam.lua
  2. +52 −12 macro.lua
  3. +55 −0 readme.md
View
94 luam.lua
@@ -10,12 +10,13 @@ require 'macro.builtin'
--- Using luam.
-- @usage follows
local usage = [[
-LuaMacro 2.1, a Lua macro preprocessor and runner
+LuaMacro 2.2, a Lua macro preprocessor and runner
-l require a library
-e statement to be executed
-c error context to be shown (default 2)
-d dump preprocessed output to stdout
-C C lexer
+ -N No #line directives when generating C
-i interactive prompt
<input> Lua source file
]]
@@ -68,25 +69,80 @@ for k,v in pairs(takes_value) do
end
end
+---------- compiling and running the output ------
+-- the tricky bit here is presenting the errors so that they refer to the
+-- original line numbers. In addition, we also present a few lines of context
+-- in the output.
-local function runstring (code,name,...)
+local function lookup_line (lno,li)
+ for i = 1,#li-1 do
+ --print(li[i].il,li[i].ol)
+ if lno < li[i+1].ol then
+ return li[i].il + (lno - li[i].ol) - 1
+ end
+ end
+ return
+end
+
+-- iterating over all lines in a string can be awkward;
+-- gmatch doesn't handle the empty-line cases properly.
+local function split_nl (t)
+ local k1 = 1
+ local k2 = t:find ('[\r\n]',k1)
+ return function()
+ if not k2 then return nil end
+ local res = t:sub(k1,k2-1)
+ k1 = k2+1
+ k2 = t:find('[\r\n]',k1)
+ return res
+ end
+end
+
+local function fix_error_trace (err,li)
+ local strname,lno = err:match '%[string "(%S+)"%]:(%d+)'
+ --print(strname,lno)
+ local ino
+ if strname then
+ lno = tonumber(lno)
+ if li then
+ ino = lookup_line(lno,li)
+ err = err:gsub('%[string "%S+"%]:'..lno..':',strname..':'..ino)
+ end
+ end
+ return err,lno,ino
+end
+
+local function runstring (code,name,li,...)
local res,err = loadstring(code,name)
+ local lno,ok
if not res then
- local lno = err:match(':(%d+):')
- lno = tonumber(lno)
- local l1,l2 = lno-args.c,lno+args.c
- local l = 1
- for line in code:gmatch '[^\n]+' do
- if l >= l1 and l <= l2 then
- if l == lno then io.write('*') end
- print(l,line)
+ err,lno,ino = fix_error_trace(err,li)
+ if ino then
+ print 'preprocessed context of error:'
+ local l1,l2 = lno-args.c,lno+args.c
+ local l = 1
+ for line in split_nl(code) do
+ if l >= l1 and l <= l2 then
+ if l == lno then io.write('*') else io.write(' ') end
+ print(l,line)
+ end
+ l = l + 1
end
- l = l + 1
end
io.stderr:write(err,'\n')
os.exit(1)
end
- return res(...)
+ ok,err = xpcall(function(...) return res(...) end, debug.traceback)
+ if not ok then
+ err = err:gsub("%[C%]: in function 'xpcall'.+",'')
+ if li then
+ repeat
+ err,lno = fix_error_trace(err,li)
+ until not lno
+ end
+ io.stderr:write(err,'\n')
+ end
+ return ok
end
local function subst (ins,name)
@@ -95,16 +151,20 @@ local function subst (ins,name)
buf[i] = v
i = i + 1
end}
- macro.substitute(ins,outf,name,args.C)
- return table.concat(buf)
+ local C
+ if args.C then
+ C = args.N and true or 'line'
+ end
+ local line_info = macro.substitute(ins,outf,name,C)
+ return table.concat(buf),line_info
end
local function subst_runstring (ins,name,...)
- local buf = subst(ins,name)
+ local buf,li = subst(ins,name)
if args.d or args.C then
print(buf)
else
- return runstring(buf,name,...)
+ return runstring(buf,name,li,...)
end
end
@@ -136,7 +196,7 @@ end
local function interactive_loop ()
os.execute(arg[-1]..' -v') -- for the Lua copyright
- print 'Lua Macro 2.1 Copyright (C) 2007-2011 Steve Donovan'
+ print 'Lua Macro 2.2 Copyright (C) 2007-2011 Steve Donovan'
local function readline()
io.write(_PROMPT or '> ')
View
64 macro.lua
@@ -565,6 +565,10 @@ end
-- @param action function to be called when keyword is encountered
-- @return previous handler associated with this keyword
function M.keyword_handler (word,action)
+ if word == 'BEGIN' or word == 'END' then
+ keyword_handlers[word] = action
+ return
+ end
if keywords[word] and keywords[word] ~= 'hook' then return end
if action then
keywords[word] = 'hook'
@@ -609,6 +613,19 @@ function M.assert(expr,msg)
if not expr then M.error(msg or 'internal error') end
end
+local line_updater, line_table
+
+local function lua_line_updater (iline,oline)
+ if not line_table then line_table = {} end
+ append(line_table,{il=iline,ol=oline})
+end
+
+local function c_line_updater (iline,oline,out,last_t,last_v)
+ local endt = last_t == 'space' and last_v or '\n'
+ out:write('#line '..iline..' "'..M.filename..'"'..endt)
+end
+
+
--- Do a macro substitution on Lua source.
-- @param src Lua source (either string or file-like reader)
-- @param out output (a file-like writer)
@@ -617,28 +634,43 @@ function M.substitute(src,out,name, use_c)
lexer = require 'macro.clexer'
scan_code = lexer.scan_c
keywords = c_keywords
+ if use_c == 'line' then
+ line_updater = c_line_updater
+ else
+ line_updater = function() end
+ end
else
lexer = require 'macro.lexer'
scan_code = lexer.scan_lua
keywords = lua_keywords
+ line_updater = lua_line_updater
end
local tok = scan_code(src,name)
+ local iline,iline_changed = 0
+ local last_t,last_v = 'space','\n'
M.filename = name or '(tmp)'
+ local t,v = tok()
+
-- this function get() is always used, so that we can handle end-of-stream properly.
-- The substitution mechanism pushes a new stream on the tstack, which is popped
-- when empty.
local tstack = {}
local push,pop = table.insert,table.remove
local function get ()
+ last_t,last_v = t,v
local t,v = tok()
while not t do
tok = pop(tstack)
if tok == nil then return nil end -- finally finished
t,v = tok()
end
+ if name == lexer.name and iline ~= lexer.line then
+ iline = lexer.line -- input line has changed
+ iline_changed = last_v
+ end
return t,v
end
@@ -693,15 +725,18 @@ function M.substitute(src,out,name, use_c)
return pass_through
end
- local t,v = tok()
- local last_t,last_v
+
local multiline_tokens,sync = lexer.multiline_tokens,lexer.sync
- local line,last_diff = 1,0
+ local line,last_diff = 0,0
function M.last_token()
return last_t,last_v
end
+ if keyword_handlers.BEGIN then
+ keyword_handlers.BEGIN()
+ end
+
while t do
local dump = true
if t == 'iden' then -- classic name macro
@@ -742,22 +777,27 @@ function M.substitute(src,out,name, use_c)
end
end
if dump then
- out:write(v)
- if multiline_tokens[t] then
+ if multiline_tokens[t] then -- track output line
line = sync(line, v)
- if M.filename == lexer.name then
- local diff = line - lexer.line
- if diff ~= last_diff then
- --print(line,lexer.line)
- last_diff = diff
- end
+ end
+ if iline_changed then
+ local diff = line - iline
+ if diff ~= last_diff then
+ line_updater(iline,line,out,last_t,last_v)
+ last_diff = diff
end
+ iline_changed = nil
end
+ out:write(v)
end
- last_t,last_v = t,v
+ -- last_t,last_v = t,v
t,v = get()
end
+ if keyword_handlers.END then
+ keyword_handlers.END()
+ end
+ return line_table
end
--- take some Lua source and return the result of the substitution.
View
55 readme.md
@@ -404,6 +404,61 @@ A more elaborate experiment is `cskin.lua` in the tests directory. This translat
t = {a,b}
}
+### Preprocessing C
+
+With the 2.2 release, LuaMacro can preprocess C files, by the inclusion of a C Lpeg lexer based on work by Peter Odding. This may seem a semi-insane pursuit, given that C already has a preprocessor, (which is widely considered a misfeature.) However, the macros we are talking about are clever, they can maintain state, and can be scoped lexically.
+
+One of the irritating things about C is the need to maintain separate include files. It would be better if we could write a module like this:
+
+
+ // dll.c
+ #include "dll.h"
+
+ export {
+ typedef struct {
+ int ival;
+ } MyStruct;
+ }
+
+ export int one(MyStruct *ms) {
+ return ms->ival + 1
+ }
+
+ export int two(MyStruct *ms) {
+ return 2*ms->ival;
+ }
+
+and have the preprocessor generate an apppropriate header file:
+
+
+ #ifndef DLL_H
+ #define DLL_H
+ typedef struct {
+ int ival;
+ } MyStruct;
+
+ int one(MyStruct *ms) ;
+ int two(MyStruct *ms) ;
+ #endif
+
+The macro `export` is straightforward:
+
+
+ M.define('export',function(get)
+ local t,v = get:next()
+ local decl,out
+ if v == '{' then
+ decl = tostring(get:upto '}')
+ f:write(decl,'\n')
+ else
+ decl = v .. ' ' .. tostring(get:upto '{')
+ f:write(decl,';\n')
+ out = decl .. '{'
+ end
+ return out
+ end)
+
+It looks ahead and if it finds a `{}` block it writes it wholesale to a file stream; otherwise writes everything upto a block opening.
### Implementation

0 comments on commit 67cfc55

Please sign in to comment.