Skip to content

Instantly share code, notes, and snippets.

@fperrad
Last active March 29, 2024 10:42
Show Gist options
  • Save fperrad/dbf7e101c633535e5beabcec98b96b0d to your computer and use it in GitHub Desktop.
Save fperrad/dbf7e101c633535e5beabcec98b96b0d to your computer and use it in GitHub Desktop.
picol
proc fib {x} {
if {<= $x 1} {
return 1
} else {
+ [fib [- $x 1]] [fib [- $x 2]]
}
}
puts [fib 20]
set a "pu"
set b {ts}
$a$b "Hello World!"
#!/bin/env lua
-- Tcl in ~ 500 lines of code.
local Parser = {}
function Parser:next()
self.pos = self.pos + 1
self.c = string.sub(self.text, self.pos, self.pos)
end
function Parser:parse_sep()
while string.match(self.c, '[ \t\n\r]') do
self:next()
end
self.type = 'SEP'
end
function Parser:parse_eol()
while string.match(self.c, '[ \t\n\r;]') do
self:next()
end
self.type = 'EOL'
end
function Parser:parse_command()
self:next()
local level = 1
local start = self.pos
local blevel = 0
while self.c ~= '' do
if self.c == '\\' then
self:next()
elseif self.c == '{' then
blevel = blevel + 1
elseif self.c == '}' then
if blevel > 0 then
blevel = blevel - 1
end
elseif self.c == '[' and blevel == 0 then
level = level + 1
elseif self.c == ']' and blevel == 0 then
level = level - 1
if level == 0 then
self:next()
break
end
end
self:next()
end
self.token = string.sub(self.text, start, self.pos - 2)
self.type = 'CMD'
end
function Parser:parse_var()
self:next()
local start = self.pos
while string.match(self.c, '[%w_]') do
self:next()
end
self.token = string.sub(self.text, start, self.pos - 1)
if self.token == '' then
self.token = '$'
self.type = 'STR'
else
self.type = 'VAR'
end
end
function Parser:parse_brace()
self:next()
local level = 1
local start = self.pos
while self.c ~= '' do
if self.c == '\\' then
self:next()
elseif self.c == '{' then
level = level + 1
elseif self.c == '}' then
level = level - 1
if level == 0 then
self:next()
break
end
end
self:next()
end
self.token = string.sub(self.text, start, self.pos - 2)
self.type = 'STR'
end
function Parser:parse_string()
local new_word = self.type == 'SEP' or self.type == 'EOL' or self.type == 'STR'
if new_word then
if self.c == '{' then
return self:parse_brace()
elseif self.c == '"' then
self.inside_quote = true
self:next()
end
end
local start = self.pos
while self.c ~= '' do
if self.c == '\\' then
self:next()
elseif self.c == '$' or self.c == '[' then
self.type = 'ESC'
self.token = string.sub(self.text, start, self.pos - 1)
return
elseif string.match(self.c, '[ \t\n\r;]') then
if not self.inside_quote then
self.token = string.sub(self.text, start, self.pos - 1)
self.type = 'ESC'
return
end
elseif self.c == '"' then
if self.inside_quote then
self:next()
self.token = string.sub(self.text, start, self.pos - 2)
self.inside_quote = false
self.type = 'ESC'
return
end
end
self:next()
end
self.token = string.sub(self.text, start, self.pos - 1)
self.type = 'ESC'
end
function Parser:parse_comment()
while self.c ~= '\n' and self.c ~= '' do
self:next()
end
end
function Parser:get_token()
while self.c ~= '' do
if string.match(self.c, '[ \t\r]') then
if self.inside_quote then
return self:parse_string()
else
return self:parse_sep()
end
elseif string.match(self.c, '[\n;]') then
if self.inside_quote then
return self:parse_string()
else
return self:parse_eol()
end
elseif self.c == '[' then
return self:parse_command()
elseif self.c == '$' then
return self:parse_var()
elseif self.c == '#' then
if self.type == 'EOL' then
self:parse_comment()
else
return self:parse_string()
end
else
return self:parse_string()
end
end
if self.type ~= 'EOL' and self.type ~= 'EOF' then
self.type = 'EOL'
else
self.type = 'EOF'
end
end
function Parser.new(text)
local o = setmetatable({
text = text,
pos = 0,
type = 'EOL',
inside_quote = false,
}, { __index = Parser })
o:next()
return o
end
local Interp = {}
local function to_number(s)
return tonumber(s) or 0
end
local function to_boolean(s)
return tonumber(s) ~= 0
end
local escape = {
['\\a'] = '\a',
['\\b'] = '\b',
['\\f'] = '\f',
['\\n'] = '\n',
['\\r'] = '\r',
['\\t'] = '\t',
['\\v'] = '\v',
}
function Interp:eval(text)
self.result = ''
local args = {}
local p = Parser.new(text)
local prev = p.type
p:get_token()
while p.type ~= 'EOF' do
while p.type == 'SEP' do
prev = p.type
p:get_token()
end
local v = p.token
if p.type == 'VAR' then
v = self.call_frame[v]
if not v then
self.result = "No such variable '" .. p.token .. "'"
return 'ERR'
end
elseif p.type == 'CMD' then
local ret = self:eval(v)
if ret ~= 'OK' then
return ret
end
v = self.result
elseif p.type == 'ESC' then
v = string.gsub(v, '\\[abfnrtv]', escape)
v = string.gsub(v, '\\x(%x%x)', function(s)
return string.char(tonumber(s, 16))
end)
end
if p.type == 'EOL' then
if #args > 0 then
local cmd = self.commands[args[1]]
if not cmd then
self.result = "No such command '" .. args[1] .. "'"
return 'ERR'
end
local ret = cmd(self, args)
if ret ~= 'OK' then
return ret
end
args = {}
end
else
if prev == 'SEP' or prev == 'EOL' then
args[#args + 1] = v
else
args[#args] = args[#args] .. v
end
end
prev = p.type
p:get_token()
end
return 'OK'
end
function Interp:arity_err(name)
self.result = "Wrong number of args for " .. name
return 'ERR'
end
function Interp:register_core_commands()
self.commands['+'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) + to_number(args[3]))
return 'OK'
end
self.commands['-'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) - to_number(args[3]))
return 'OK'
end
self.commands['*'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) * to_number(args[3]))
return 'OK'
end
self.commands['/'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) / to_number(args[3]))
return 'OK'
end
self.commands['>'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) > to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['>='] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) >= to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['<'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) < to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['<='] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) <= to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['=='] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) == to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['!='] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) ~= to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['set'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.call_frame[args[2]] = args[3]
interp.result = args[3]
return 'OK'
end
self.commands['puts'] = function(interp, args)
if #args ~= 2 then
return interp:arity_err(args[1])
end
io.stdout:write(args[2], '\n')
return 'OK'
end
self.commands['if'] = function(interp, args)
if #args ~= 3 and #args ~= 5 then
return interp:arity_err(args[1])
end
local ret = interp:eval(args[2])
if ret ~= 'OK' then
return ret
end
if to_boolean(interp.result) then
return interp:eval(args[3])
elseif #args == 5 then
return interp:eval(args[5])
end
return 'OK'
end
self.commands['while'] = function(interp, args)
if #args ~= 3 then
return interp:arity_err(args[1])
end
while true do
local ret = interp:eval(args[2])
if ret ~= 'OK' then
return ret
end
if to_boolean(interp.result) then
ret = interp:eval(args[3])
if ret == 'BREAK' then
return 'OK'
elseif ret ~= 'OK' and ret ~= 'CONTINUE' then
return ret
end
else
return 'OK'
end
end
end
self.commands['break'] = function(interp, args)
if #args ~= 1 then
return interp:arity_err(args[1])
end
return 'BREAK'
end
self.commands['continue'] = function(interp, args)
if #args ~= 1 then
return interp:arity_err(args[1])
end
return 'CONTINUE'
end
self.commands['return'] = function(interp, args)
if #args ~= 1 and #args ~= 2 then
return interp:arity_err(args[1])
end
interp.result = args[2] or ''
return 'OK'
end
self.commands['proc'] = function(interp, args)
if #args ~= 4 then
return interp:arity_err(args[1])
end
local name = args[2]
if interp.commands[name] then
interp.result = "Command '" .. name .. "' already defined"
return 'ERR'
end
local arglist = args[3]
local body = args[4]
interp.commands[name] = function(pinterp, pargs)
local parent = pinterp.call_frame
pinterp.call_frame = setmetatable({}, { __index = parent })
local arity = 0
for pname in arglist:gmatch('[^ ]+') do
arity = arity + 1
if arity > (#pargs - 1) then
break
end
pinterp.call_frame[pname] = pargs[arity + 1]
end
if arity ~= (#pargs - 1) then
pinterp.call_frame = parent
pinterp.result = "Proc '" .. pargs[1] .. "' called with wrong arg num"
return 'ERR'
end
local ret = pinterp:eval(body)
if ret == 'RETURN' then
ret = 'OK'
end
pinterp.call_frame = parent
return ret
end
return 'OK'
end
end
function Interp.new()
local o = setmetatable({
call_frame = {},
commands = {},
result = '',
}, { __index = Interp })
o:register_core_commands()
return o
end
local interp = Interp.new()
if #arg > 0 then
local f = io.open(arg[1], 'r')
local text = f:read('a')
f:close()
local ret = interp:eval(text)
if ret ~= 'OK' then
io.stdout:write(interp.result, '\n')
end
else
while true do
io.stdout:write('picol> ')
local line = io.stdin:read('l')
if not line then
break
end
local ret = interp:eval(line)
if interp.result ~= '' then
io.stdout:write(ret, ': ', interp.result, '\n')
end
end
end
-- Tcl in ~ 500 lines of code.
local record Parser
enum TokenType
'ESC'
'STR'
'CMD'
'VAR'
'SEP'
'EOL'
'EOF'
end
text: string
pos: integer
c: string
token: string
type: TokenType
inside_quote: boolean
next: function(self: Parser): nil
parse_sep: function(self: Parser): nil
parse_eol: function(self: Parser): nil
parse_command: function(self: Parser): nil
parse_var: function(self: Parser): nil
parse_brace: function(self: Parser): nil
parse_string: function(self: Parser): nil
parse_comment: function(self: Parser): nil
get_token: function(self: Parser): nil
new: function(text: string): Parser
end
function Parser:next (): nil
self.pos = self.pos + 1
self.c = string.sub(self.text, self.pos, self.pos)
end
function Parser:parse_sep (): nil
while string.match(self.c, '[ \t\n\r]') do
self:next()
end
self.type = 'SEP'
end
function Parser:parse_eol (): nil
while string.match(self.c, '[ \t\n\r;]') do
self:next()
end
self.type = 'EOL'
end
function Parser:parse_command (): nil
self:next() -- skip the [
local level = 1
local start = self.pos
local blevel = 0
while self.c ~= '' do
if self.c == '\\' then
self:next()
elseif self.c == '{' then
blevel = blevel + 1
elseif self.c == '}' then
if blevel > 0 then
blevel = blevel - 1
end
elseif self.c == '[' and blevel == 0 then
level = level + 1
elseif self.c == ']' and blevel == 0 then
level = level - 1
if level == 0 then
self:next()
break
end
end
self:next()
end
self.token = string.sub(self.text, start, self.pos - 2)
self.type = 'CMD'
end
function Parser:parse_var (): nil
self:next() -- skip the $
local start = self.pos
while string.match(self.c, '[%w_]') do
self:next()
end
self.token = string.sub(self.text, start, self.pos - 1)
if self.token == '' then
self.token = '$'
self.type = 'STR'
else
self.type = 'VAR'
end
end
function Parser:parse_brace (): nil
self:next() -- skip the {
local level = 1
local start = self.pos
while self.c ~= '' do
if self.c == '\\' then
self:next()
elseif self.c == '{' then
level = level + 1
elseif self.c == '}' then
level = level - 1
if level == 0 then
self:next()
break
end
end
self:next()
end
self.token = string.sub(self.text, start, self.pos - 2)
self.type = 'STR'
end
function Parser:parse_string (): nil
local new_word = self.type == 'SEP' or self.type == 'EOL' or self.type == 'STR'
if new_word then
if self.c == '{' then
return self:parse_brace()
elseif self.c == '"' then
self.inside_quote = true
self:next()
end
end
local start = self.pos
while self.c ~= '' do
if self.c == '\\' then
self:next()
elseif self.c == '$' or self.c == '[' then
self.type = 'ESC'
self.token = string.sub(self.text, start, self.pos - 1)
return
elseif string.match(self.c, '[ \t\n\r;]') then
if not self.inside_quote then
self.token = string.sub(self.text, start, self.pos - 1)
self.type = 'ESC'
return
end
elseif self.c == '"' then
if self.inside_quote then
self:next()
self.token = string.sub(self.text, start, self.pos - 2)
self.inside_quote = false
self.type = 'ESC'
return
end
end
self:next()
end
self.token = string.sub(self.text, start, self.pos - 1)
self.type = 'ESC'
end
function Parser:parse_comment (): nil
while self.c ~= '\n' and self.c ~= '' do
self:next()
end
end
function Parser:get_token (): nil
while self.c ~= '' do
if string.match(self.c, '[ \t\r]') then
if self.inside_quote then
return self:parse_string()
else
return self:parse_sep()
end
elseif string.match(self.c, '[\n;]') then
if self.inside_quote then
return self:parse_string()
else
return self:parse_eol()
end
elseif self.c == '[' then
return self:parse_command()
elseif self.c == '$' then
return self:parse_var()
elseif self.c == '#' then
if self.type == 'EOL' then
self:parse_comment()
else
return self:parse_string()
end
else
return self:parse_string()
end
end
if self.type ~= 'EOL' and self.type ~= 'EOF' then
self.type = 'EOL'
else
self.type = 'EOF'
end
end
function Parser.new (text: string): Parser
local o = setmetatable({
text = text,
pos = 0,
type = 'EOL',
inside_quote = false,
}, { __index = Parser}) as Parser
o:next()
return o
end
local record Interp
enum RetCode
'OK'
'ERR'
'RETURN'
'BREAK'
'CONTINUE'
end
type Command = function(interp: Interp, args: {string}): RetCode
call_frame: {string:string}
commands: {string:Command}
result: string
eval: function(self: Interp, text: string): RetCode
arity_err: function(self: Interp, name: string): RetCode
register_core_commands: function(self: Interp)
new: function(): Interp
end
local function to_number (s: string): number
return tonumber(s) or 0
end
local function to_boolean (s: string): boolean
return tonumber(s) ~= 0
end
local escape = {
['\\a'] = '\a',
['\\b'] = '\b',
['\\f'] = '\f',
['\\n'] = '\n',
['\\r'] = '\r',
['\\t'] = '\t',
['\\v'] = '\v',
}
function Interp:eval (text: string): Interp.RetCode
self.result = ''
local args: {string} = {}
local p = Parser.new(text)
local prev = p.type
p:get_token()
while p.type ~= 'EOF' do
while p.type == 'SEP' do
prev = p.type
p:get_token()
end
local v = p.token
if p.type == 'VAR' then
v = self.call_frame[v]
if not v then
self.result = "No such variable '" .. p.token .. "'"
return 'ERR'
end
elseif p.type == 'CMD' then
local ret = self:eval(v)
if ret ~= 'OK' then
return ret
end
v = self.result
elseif p.type == 'ESC' then
v = string.gsub(v, '\\[abfnrtv]', escape)
v = string.gsub(v, '\\x(%x%x)', function (s: string): string
return string.char(tonumber(s, 16))
end)
end
if p.type == 'EOL' then
if #args > 0 then
local cmd = self.commands[args[1]]
if not cmd then
self.result = "No such command '" .. args[1] .. "'"
return 'ERR'
end
local ret = cmd(self, args)
if ret ~= 'OK' then
return ret
end
args = {}
end
else
if prev == 'SEP' or prev == 'EOL' then
args[#args+1] = v
else
args[#args] = args[#args] .. v
end
end
prev = p.type
p:get_token()
end
return 'OK'
end
function Interp:arity_err (name: string): Interp.RetCode
self.result = "Wrong number of args for " .. name
return 'ERR'
end
function Interp:register_core_commands ()
self.commands['+'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) + to_number(args[3]))
return 'OK'
end
self.commands['-'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) - to_number(args[3]))
return 'OK'
end
self.commands['*'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) * to_number(args[3]))
return 'OK'
end
self.commands['/'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = tostring(to_number(args[2]) / to_number(args[3]))
return 'OK'
end
self.commands['>'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) > to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['>='] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) >= to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['<'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) < to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['<='] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) <= to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['=='] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) == to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['!='] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.result = to_number(args[2]) ~= to_number(args[3]) and '1' or '0'
return 'OK'
end
self.commands['set'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
interp.call_frame[args[2]] = args[3]
interp.result = args[3]
return 'OK'
end
self.commands['puts'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 2 then
return interp:arity_err(args[1])
end
io.stdout:write(args[2], '\n')
return 'OK'
end
self.commands['if'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 and #args ~= 5 then
return interp:arity_err(args[1])
end
local ret = interp:eval(args[2])
if ret ~= 'OK' then
return ret
end
if to_boolean(interp.result) then
return interp:eval(args[3])
elseif #args == 5 then
return interp:eval(args[5])
end
return 'OK'
end
self.commands['while'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 3 then
return interp:arity_err(args[1])
end
while true do
local ret = interp:eval(args[2])
if ret ~= 'OK' then
return ret
end
if to_boolean(interp.result) then
ret = interp:eval(args[3])
if ret == 'BREAK' then
return 'OK'
elseif ret ~= 'OK' and ret ~= 'CONTINUE' then
return ret
end
else
return 'OK'
end
end
end
self.commands['break'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 1 then
return interp:arity_err(args[1])
end
return 'BREAK'
end
self.commands['continue'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 1 then
return interp:arity_err(args[1])
end
return 'CONTINUE'
end
self.commands['return'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 1 and #args ~= 2 then
return interp:arity_err(args[1])
end
interp.result = args[2] or ''
return 'OK'
end
self.commands['proc'] = function (interp: Interp, args: {string}): Interp.RetCode
if #args ~= 4 then
return interp:arity_err(args[1])
end
local name = args[2]
if interp.commands[name] then
interp.result = "Command '" .. name .. "' already defined"
return 'ERR'
end
local arglist = args[3]
local body = args[4]
interp.commands[name] = function (pinterp: Interp, pargs: {string}): Interp.RetCode
local parent = pinterp.call_frame
pinterp.call_frame = setmetatable({}, { __index = parent })
local arity = 0
for pname in arglist:gmatch('[^ ]+') do
arity = arity + 1
if arity > (#pargs - 1) then
break
end
pinterp.call_frame[pname] = pargs[arity + 1]
end
if arity ~= (#pargs - 1) then
pinterp.call_frame = parent
pinterp.result = "Proc '" ..pargs[1] .. "' called with wrong arg num"
return 'ERR'
end
local ret = pinterp:eval(body)
if ret == 'RETURN' then
ret = 'OK'
end
pinterp.call_frame = parent
return ret
end
return 'OK'
end
end
function Interp.new (): Interp
local o = setmetatable({
call_frame = {},
commands = {},
result = '',
}, { __index = Interp}) as Interp
o:register_core_commands()
return o
end
local interp = Interp.new()
if #arg > 0 then
local f = io.open(arg[1], 'r')
local text = f:read('a')
f:close()
local ret = interp:eval(text)
if ret ~= 'OK' then
io.stdout:write(interp.result, '\n')
end
else
while true do
io.stdout:write('picol> ')
local line = io.stdin:read('l')
if not line then
break
end
local ret = interp:eval(line)
if interp.result ~= '' then
io.stdout:write(ret, ': ', interp.result, '\n')
end
end
end
proc square {x} {
* $x $x
}
set a 1
while {<= $a 10} {
if {== $a 5} {
puts {Missing five!}
set a [+ $a 1]
continue
}
puts "I can compute that $a*$a = [square $a]"
set a [+ $a 1]
}
@fperrad
Copy link
Author

fperrad commented Mar 27, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment