-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ravi evolution proposal #223
Comments
Hi - thank you for your interest. I can't accept bounties but if you would like to contribute patches/improvements, will be happy to consider them. |
Understood, thank you for your reply. I've researched your project within a few weeks, and I have some questions and proposals. MIR JITWhy do you choose MIR JIT? Also, why we have to compile functions manually? Why not to do it at start, as LuaJIT does? PerformanceRavi is 3-5 times slower than vanilla Lua 5.3, and about 10 times slower than LuaJIT. Yes, Ravi still leaves far, far behind all smoothie technologies such as Python (with asyncio) and V8 (NodeJS). Static typingStatic typing is great thing, and you have done great job implementing this. Some types (such as
|
As I said before, I've done some work to change some confusing behaviour of typing system (and improve it a bit).Function arguments with types
|
Hi thank you for above. I will respond later when I have time. In general, please keep PRs separate by each feature - and also ensure you have run tests/added tests. One thing worth noting is we cannot allow NIL for integer/number types or table/integer[]/number[] types because that will mess up the JIT |
Yea, agree with you about table/integer[]/number[]. I made some quick tests, all looks good. ravi.jit(true)
function sum(a: integer, b: integer, c: integer?)
return a + b + (c or 0)
end
ravi.compile(sum)
for i=1,10000000 do
assert(sum(10, 20), 30)
assert(sum(10, 20, 70), 100)
end
print 'done' What do you mean about messing up the JIT? |
ravi generates special bytecodes for integer/number types when annotated
There are no NIL checks as the whole point is to do optimized arithmetic. |
Also new compiler will allocate on stack |
Still not understanding how it theoretically can affect runtime when using optional types. // optional types
vmcase(OP_RAVI_TOINT_NIL) {
lua_Integer j;
if (RAVI_LIKELY(tointeger(ra, &j))) { setivalue(ra, j); }
else if(!ttisnil(ra))
luaG_runerror(L, "TOINT: integer expected");
vmbreak;
} Can you, please, explain me the main danger with some example code? Optional typed arguments is very important thing, without it it would be hard to implement some kind of applications. |
There are two distinct goals for type annotations. Ravi's implementation is a hybrid. Some types are for performance. Others are just for argument / return type checking - but they do not help performance To support optional integer/number would need the compiler to revert to unoptimized type as NIL is not a valid value |
Can you clarify where to look? Commit number/whatever. I also still do not understand what exactly wrong with my implementation. Are there any potential critical bugs may hide? Such as crashes, memory corruption, et cetera. |
Note that this is currently handled correctly. The following code produces an ravi.jit(true)
function sum(n, a: integer, b: integer)
return (n and a or nil) + b
end
ravi.dumplua(sum) |
I think this code is not correct at all. Slightly modified code: ravi.jit(true)
function sum(n: integer?, a: integer, b: integer)
return a + b + (n or 0)
end
-- ravi.dumplua(sum)
print(sum(nil, 1, 2))
print(sum(1, 2, 3))
print(sum(1, 2)) works and returns But I have no idea on bytecode, I don't know how to read it. |
That was intended to show that in such cases the unoptimized It should show that function sum(a: integer?, b: integer)
return a + b
end is not using the |
I tried, it seems my code producing ADD_II but your code producing ADD. ravi.jit(true)
function sum(a: integer, b: integer)
return a+b
end
function sum2(n: integer?, a: integer, b: integer)
return a + b + (n or 0)
end
function sum3(n, a: integer, b: integer)
return (n and a or nil) + b
end
print('--------------------------------------------')
print [[function sum(a: integer, b: integer)
return a+b
end]]
ravi.dumplua(sum)
print('--------------------------------------------')
print [[function sum2(n: integer?, a: integer, b: integer)
return a + b + (n or 0)
end]]
ravi.dumplua(sum2)
print('--------------------------------------------')
print [[function sum3(n, a: integer, b: integer)
return (n and a or nil) + b
end]]
ravi.dumplua(sum3) result:
|
All of the code seems to be correct. For the second case
are missing. function sum(a: integer?, b: integer)
return a + b
end I didn't apply your patch so far. |
Also these opcodes might be redundant - as the the existing ones already allow NIL where applicable |
Re reading bytecodes - you may want to checkout https://the-ravi-programming-language.readthedocs.io/en/latest/lua_bytecode_reference.html. It doesn't cover the extended Ravi opcodes but these are straight forward once you can follow the Lua 5.3 ones |
The existing ones are patched to not allow nil any more. See the patch supplied in #223 (comment). |
yeah, missed it, thanks.
Of course it produces ADD, but as I mentioned before, code is incorrect itself. Another correct variant (also produces ADDII) ravi.jit(true)
function sum(a: integer?, b: integer)
return @integer(a) + b
end
ravi.dumplua(sum) but it is almost the same as By the way, main purpose of optional typed arguments is to ensure that external variable (if supplied) is correct type. I mean, function hello(s: string?)
s = s or 'world'
return "hello, " .. s
end Without support of optional arguments, we have to do something like that: function hello(s)
s = s or 'world'
assert(type(s), 'string')
return "hello, " .. s
end |
For optional integer/number you will then need to set the value to 0 rather than NIL - and ensure type is set. Then I think it might be okay. |
Note that function hello(s: string?)
s = s or 'world'
return "hello, " .. s
end is not optimal since function hello(s: string?)
local rs: string = s or 'world'
return "hello, " .. rs
end |
That's what I said in original post. function hello(s: string)
return "hello, " .. s
end
print(hello("world")) -- works as intended
print(hello(false)) -- works as intended, throws `ravi: r.lua:0: string expected`
print(hello()) -- crash -- `ravi: r.lua:2: attempt to concatenate a nil value (local 's')` For some reason, |
Well - actually a few of the types are union of type & NIL. @XmiliaH made this more explicit in the code.
|
Also a solution. You won't be able to set default variables with |
I had a todo #172 |
I also confused with that. |
We can tighten the check for annotated types to disallow NIL. I didn't do it originally as I took the view that NIL is a valid value for these (i.e. absent) |
Also, compromise solution may be to implement union types. They we may explicitly write function sum(a: integer, b: integer|nil)
return @integer(a) + b
end when needed (and when we know what we doing). And also If it's possible not for a big cost for parser, maybe add possibility to specify literals directly, i.e. function sum(a: integer, b: integer|666)
return a + b
end
function hello(s: string|"World")
return 'hello ' .. s
end I'm not sure that it is possible without lot of workarounds, but I might be wrong. |
@snoopcatt maybe change the type parsing to: if (strcmp(str, "integer") == 0)
tm = RAVI_TM_INTEGER;
else if (strcmp(str, "number") == 0)
tm = RAVI_TM_FLOAT;
else if (strcmp(str, "closure") == 0)
tm = RAVI_TM_FUNCTION_OR_NIL;
else if (strcmp(str, "table") == 0)
tm = RAVI_TM_TABLE;
else if (strcmp(str, "string") == 0)
tm = RAVI_TM_STRING_OR_NIL;
else if (strcmp(str, "boolean") == 0)
tm = RAVI_TM_BOOLEAN_OR_NIL;
else if (strcmp(str, "any") == 0)
tm = RAVI_TM_ANY;
else {
/* default is a userdata type */
tm = RAVI_TM_USERDATA_OR_NIL;
typename = user_defined_type_name(ls, typename);
str = getstr(typename);
*pusertype = typename;
}
if (tm == RAVI_TM_FLOAT || tm == RAVI_TM_INTEGER) {
/* if we see [] then it is an array type */
if (testnext(ls, '[')) {
checknext(ls, ']');
tm = (tm == RAVI_TM_FLOAT) ? RAVI_TM_FLOAT_ARRAY : RAVI_TM_INTEGER_ARRAY;
}
}
if (testnext(ls, '?')) {
tm |= RAVI_TM_NIL;
} else if (testnext(ls, '!')) {
tm &= ~RAVI_TM_NIL;
} this would still have the old behavior to not break compatibility but allow to remove the nil type with |
All we need is to enable types annotation inside tables the same way as for local variables: Looks like simple and elegant solution, but I have no idea what about technical side and why only local variables can be annotated for now |
I would say it does not - depending on what we mean by Lua spirit. I am using the term as it is used by C standard - spirit of C. Lua's implementation is carefully designed to avoid heap allocation in the parser. It uses recursion and stack to create temp expression nodes. Typed Lua is an add-on - so immediately add a lot of overhead and breaks the spirit of Lua in that sense. You can look at new project https://github.com/teal-language/tl or Pallene - they may be what you are looking for. |
Annotating tables is quite hard and checking it at runtime too. |
I meant only how it looks for the "end customer". Simple and elegant. |
local a: integer = 1
local b: string = 'hello'
local t = {a=a, b=b} why can't we say |
First: following the current type system it should be more like: local a: integer = 1
local b: string = 'hello'
local t: {a: integer, b: string} = {a = 1, b = 'hello'} Second: every access would need to check the types since you could pass it to a function with violates this constraints and writes an string to the key function f(t: {a: {b: {c: {d: {e: integer}}}}})
return t
end |
I mean, why it is possible to annotate only local variables? Or it is Lua design limitations? |
Globales are stored in the global table accessible with global a: integer; -- just assuming to annotate the global `a`
_G.a = "not an integer"
print(a) should |
@snoopcatt I just noticed that you forgot to change the JIT compiler in |
this, same as for local variables: ➜ ~ cat t.lua
local a: integer = 1
local b: string = 'hello'
a = "not integer"
yes, but we already have some runtime checks, on function calls for example. that's what about my question about cost was. I didn't dive very deep into Lua, I only know basics, i.e. how to create C module etc. I thought it is possible to bring some runtime checks at for example static void assignment (LexState *ls, struct LHS_assign *lh, int nvars) to ensure new value is still correct type for that variable (we storing type of variable somewhere, aren't we?) |
The type for local variables is stored, however, the types of table values are not. |
No more questions ☺
Thanks. static void emit_op_tostring(struct function *fn, int A, int pc) {
(void)pc;
emit_reg(fn, "ra", A);
membuff_add_string(&fn->body, "if (!ttisstring(ra)) {\n");
#if GOTO_ON_ERROR
membuff_add_fstring(&fn->body, " error_code = %d;\n", Error_string_expected);
membuff_add_string(&fn->body, " goto Lraise_error;\n");
#else
membuff_add_fstring(&fn->body, " raviV_raise_error(L, %d);\n", Error_string_expected);
#endif
membuff_add_string(&fn->body, "}\n");
} in oppose to vmcase(OP_RAVI_TOSTRING) {
if (!ttisnil(ra) && RAVI_UNLIKELY(!ttisstring(ra)))
luaG_runerror(L, "string expected");
vmbreak;
} Wut? Why? Why even if JIT enabled --
I think I'm totally lost here |
The same behavior can be seen for |
Oh, I'm not (fully) lost my mind diving into it, it just a bug.
So, now we have to change behaviour of that anyway. |
In that case I would use if (strcmp(str, "integer") == 0)
tm = RAVI_TM_INTEGER;
else if (strcmp(str, "number") == 0)
tm = RAVI_TM_FLOAT;
else if (strcmp(str, "closure") == 0)
tm = RAVI_TM_FUNCTION;
else if (strcmp(str, "table") == 0)
tm = RAVI_TM_TABLE;
else if (strcmp(str, "string") == 0)
tm = RAVI_TM_STRING;
else if (strcmp(str, "boolean") == 0)
tm = RAVI_TM_BOOLEAN;
else if (strcmp(str, "any") == 0)
tm = RAVI_TM_ANY;
else {
/* default is a userdata type */
tm = RAVI_TM_USERDATA;
typename = user_defined_type_name(ls, typename);
str = getstr(typename);
*pusertype = typename;
}
if (tm == RAVI_TM_FLOAT || tm == RAVI_TM_INTEGER) {
/* if we see [] then it is an array type */
if (testnext(ls, '[')) {
checknext(ls, ']');
tm = (tm == RAVI_TM_FLOAT) ? RAVI_TM_FLOAT_ARRAY : RAVI_TM_INTEGER_ARRAY;
}
}
if (testnext(ls, '?')) {
tm |= RAVI_TM_NIL;
} This allows a consistent use of |
I'm afraid it won't work. I've done it by just copy-pasting opcodes and appending _NIL to them, it works, but it's dirty solution, I'm sure there are more graceful way to do that. I've imported my working repos to sh*thub (my god, it still can't mirror git repos, in 2021 year) |
We could enhance the existing bytecode with a parameter as it doesn't use them I think Each bytecode can take upto 3 params A,B,C. The TO* bytecodes just use A I think |
But there is more complication because we have type checks that don't use TO* bytecodes. These need to work too . I mean the checks at compile time and also for upvalue assigments |
I would propose changing |
@snoopcatt Re the performance of Ravi vs Lua53 vs LuaJIT; In interpreted code, Ravi should be better than Lua 5.3 but worse than Lua 5.4 for regular Lua code. JIT only makes sense for type annotated code - as otherwise the small JIT engine cannot sufficiently optimize code, and more time is spent compiling the code too, depending on whether you are reusing functions or not. LuaJIT is much better in the general case as it runs on any Lua code and can perform well. LuaJIT also has negligible time spent compiling code. I am interested to know how you got your figures if you are able to share. |
Re roadmap/plan - here are some things I would like to do:
Some harder problems:
|
Why MIR? I implemented multiple JIT backends, including LLVM, libgccjit, NanoJIT, Eclipse OMR. For something that is like Lua, it is necessary to have a small JIT engine. MIR is the only engine that compiles in 2 secs, is small like Lua, and yet has an optimizer that can achieve good performance if the code is generated carefully. |
Finally re the changes we have been discussing - please allow me some time to look at PRs as I don't have time to work on Ravi except during weekends (and that too it has to compete with other hobby projects). I should add that I appreciate the discussion and the contributions. It will be made easier for me if you separate out each feature when submitting PRs. |
of course: listener.lua deps:
results: |
Of course. I'm not ready to submit PRs for such fundamentl things, I just shown you proof-of-concept. I've sent some PRs with small changes that most likely they won't break anything (of course separated from each other) By the way, if it `s not a secret, why u refused paid working on some features? 👀 |
Thank for sharing your test case. I will have a look and let you know what I think |
Ravi is a hobby project that I work on for my own pleasure. It is not a job I do. |
That's nothing about a job. Nothing about deadlines and tasks. So, I've opened issues for each thing separately: #228 #229 (and #226 #227 also) If these proposals are OK to you — please, add [enhancement] tags to issues and we may start experimenting around implementations. I won't try to open PRs myself, I think such core things must be done by someone who knows Ravi architecture really deep. But if someone wants to — not only you, maybe @XmiliaH (or anyone else) -- I really would like to donate some coins for quality code that will be merged to upstream ☺ Thank you all for the discussion! |
Hello!
We have some proposals about Ravi development & support.
Our team wish to use Ravi in production.
So, if you need some our help & contributing (patches, bounties, etc.) we need to talk with you somewhere ☺
If interested, please, share some your contact.
The text was updated successfully, but these errors were encountered: