Skip to content

Commit 65992d2

Browse files
Upload newest offline version (1.0.2)
1 parent eed8a7b commit 65992d2

File tree

13 files changed

+863
-228
lines changed

13 files changed

+863
-228
lines changed

README.md

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ This is a Lua 5.3 VM and compiler written in [Go](http://golang.org/). This is i
66
programs, with minimal fuss and bother.
77

88
I have been using this VM/compiler as the primary script host in Rubble (a scripted templating system used to generate
9-
data files for the game Dwarf Fortress) for over a year now, so they are fairly well tested. Speaking of tests: No formal
10-
unit tests are provided. Testing was done by running under real-world conditions with Rubble, and in Rubble's built-in
11-
template testing system (it is not appropriate to distribute these tests with this library for obvious reasons).
9+
data files for the game Dwarf Fortress) for over a year now, so they are fairly well tested. In addition to the real-world
10+
"testing" that this has received I am slowly adding proper tests based on the official Lua test suite. These tests are
11+
far from complete, but are slowly getting more so as time passes.
1212

1313
Most (if not all) of the API functions may cause a panic, but only if things go REALLY wrong. If a function does not
1414
state that it can panic or "raise an error" it will only do so if a critical internal assumption proves to be wrong
@@ -47,16 +47,45 @@ If you want to use a third-party compiler it will need to produce binaries with
4747

4848
When building the reference compiler on most systems these settings should be the default.
4949

50+
The VM API has a function that wraps `luac` to load code, but the way it does this may or may not fit your needs. To use
51+
this wrapper you will need to have `luac` on your path or otherwise placed so the VM can find it. See the documentation
52+
for `State.LoadTextExternal` for more information. Keep in mind that due to limitations in Go and `luac`, this function
53+
is not reentrant! If you need concurrency support it would be better to use `State.LoadBinary` and write your own wrapper.
54+
5055
The default compiler provided by this library does not support constant folding, and some special instructions are not
5156
used at all (instead preferring simpler sequences of other instructions). For example TESTSET is never generated, TEST
5257
is used in all cases (largely because it would greatly complicate the compiler if I tried to use TESTSET where possible).
5358
Expressions use a simple "recursive" code generation style, meaning that it wastes registers like crazy in some (rare)
5459
cases.
5560

61+
One of the biggest code quality offenders is `or` and `and`, as they can result in sequences like this one:
62+
63+
[4] LT A:1 B:r(0) C:k(2) ; CK:5
64+
[5] JMP A:0 SBX:1 ; to:7
65+
[6] LOADBOOL A:2 B:1 C:1
66+
[7] LOADBOOL A:2 B:0 C:0
67+
[8] TEST A:2 C:1
68+
[9] JMP A:0 SBX:7 ; to:17
69+
[10] EQ A:1 B:r(1) C:k(3) ; CK:<nil>
70+
... (7 more instructions to implement next part of condition)
71+
72+
As you can see this is terrible. That sequence would be better written as:
73+
74+
[4] LT A:1 B:r(0) C:k(2) ; CK:5
75+
[5] JMP A:0 SBX:2 ; to:8
76+
[6] EQ A:1 B:r(1) C:k(3) ; CK:<nil>
77+
... (1 more instruction to implement next part of condition)
78+
79+
But the current expression compiler is not smart enough to do it that way. Luckily this is the worst offender, most
80+
things produce code that is very close or identical to what `luac` produces. Note that the reason why this code is so
81+
bad is entirely because the expression used `or` (and the implementation of `and` and `or` is very bad).
82+
5683
The compiler provides an implementation of a `continue` keyword, but the keyword definition in the lexer is commented
5784
out. If you want `continue` all you need to do is uncomment the indicated line (near the top of `ast/lexer.go`). There
5885
is also a flag in the VM that *should* make tables use 0 based indexing. This feature has received minimal testing, so
5986
it probably doesn't work properly. If you want to try 0 based indexing just set the variable `TableIndexOffset` to 0.
87+
Note that `TableIndexOffset` is strictly a VM setting, the standard modules do not respect this setting (for example the
88+
`table` module and `ipairs` will still insist on using 1 as the first index).
6089

6190

6291
Missing Stuff:
@@ -142,6 +171,34 @@ to rework the error handler...
142171
Changes:
143172
------------------------------------------------------------------------------------------------------------------------
144173

174+
1.0.2
175+
176+
More tests, more (compiler) bugs fixed. Damn compiler will be the death of me yet...
177+
178+
In addition to the inevitable compiler bugs I also fixed the way the VM handles upvalues. Before I was giving each
179+
closure its own copy of each upvalue, so multiple closures never properly shared values. This change fixes several
180+
subtle (and several not so subtle) bugs.
181+
182+
Oh, and `pcall` works now (it didn't work at all before. Sorry, I never used it).
183+
184+
* Added more script tests. I still have a lot more to do... (script_test.go)
185+
* Fixed incorrect compilation of method declarations (`function a:x() end`). Depressingly the issue was only one
186+
incorrect word, but it resulted in *very* wrong results (I am really starting to remember why I hated writing the
187+
compiler, the VM was fun, the compiler... not.) (ast/parse.go)
188+
* Parenthesized expression that would normally (without the parenthesis) return multiple values (for example: `(...)`)
189+
were not properly truncating the result to a single value. (compile_expr.go)
190+
* Fixed a semi-major VM issue with upvalues. Closures that should have a single shared upvalue were instead each using
191+
their own private copy after said upvalue was closed. This required an almost total rewrite of the way upvalues are
192+
stored internally. (all over the place, but mainly callframe.go, function.go, api.go, and vm.go)
193+
* JMP instructions created by `break` and `continue` statements are now properly patched by the compiler to close any
194+
upvalues there may be. (compile.go)
195+
* Fixed the `pcall` script function so it actually works. (lmodbase/functions.go)
196+
* On a recovered error each stack frame's upvalues are closed before the stack is stripped. This corrects incorrect
197+
behavior that arises when a function stores a closure to an unclosed upvalue then errors out (the closure may still be
198+
referenced, but it's upvalues may be invalid). (api.go, callframe.go)
199+
200+
* * *
201+
145202
1.0.1
146203

147204
This version adds a bunch of tests (still not nearly as many as I would like), and fixes a ton of minor compiler errors.

api.go

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,26 @@ func (l *State) Push(v interface{}) {
5555
case func(l *State) int:
5656
v = &function{
5757
native: v2,
58-
uvDefs: []upValue{{name: "_ENV", index: -1}},
59-
uvClosed: []bool{true},
60-
upVals: []value{l.global},
61-
uvAbsIdxs: []int{-1},
58+
up: []*upValue{{
59+
name: "_ENV",
60+
index: -1,
61+
closed: true,
62+
val: l.global,
63+
absIdx: -1,
64+
},
65+
},
6266
}
6367
case NativeFunction:
6468
v = &function{
6569
native: v2,
66-
uvDefs: []upValue{{name: "_ENV", index: -1}},
67-
uvClosed: []bool{true},
68-
upVals: []value{l.global},
69-
uvAbsIdxs: []int{-1},
70+
up: []*upValue{{
71+
name: "_ENV",
72+
index: -1,
73+
closed: true,
74+
val: l.global,
75+
absIdx: -1,
76+
},
77+
},
7078
}
7179
default:
7280
v = &userData{
@@ -88,27 +96,27 @@ func (l *State) PushClosure(f NativeFunction, v ...int) {
8896

8997
fn := &function{
9098
native: f,
91-
uvDefs: make([]upValue, c),
92-
uvClosed: make([]bool, c),
93-
upVals: make([]value, c),
94-
uvAbsIdxs: make([]int, c),
99+
up: make([]*upValue, c),
95100
}
96101

97102
// ALL native functions ALWAYS have their first upvalue set to the global table.
98103
// This differs from standard Lua, but doesn't hurt anything.
99-
fn.uvDefs[0].name = "_ENV"
100-
fn.uvDefs[0].index = 0
101-
fn.uvClosed[0] = true
102-
fn.upVals[0] = l.global
103-
fn.uvAbsIdxs[0] = -1
104+
fn.up[0] = &upValue{
105+
name: "_ENV",
106+
index: -1,
107+
closed: true,
108+
val: l.global,
109+
absIdx: -1,
110+
}
104111

105112
for i := 1; i < c; i++ {
106-
fn.uvDefs[i].name = "(native upvalue)"
107-
fn.uvDefs[i].index = -1
108-
fn.uvClosed[i] = true
109-
val := l.get(v[i-1])
110-
fn.upVals[i] = val
111-
fn.uvAbsIdxs[i] = -1
113+
fn.up[i] = &upValue{
114+
name: "(native upvalue)",
115+
index: -1,
116+
closed: true,
117+
val: l.get(v[i-1]),
118+
absIdx: -1,
119+
}
112120
}
113121

114122
l.stack.Push(fn)
@@ -518,16 +526,22 @@ func (l *State) ForEachInTable(t int, f func()) {
518526
// Other
519527

520528
// SetUpVal sets upvalue "i" in the function at "f" to the value at "v".
521-
// If the upvalue index is out of range or "f" is not a function false is returned
522-
// and nothing is done, else returns true and sets the upval.
529+
// If the upvalue index is out of range, "f" is not a function, or the upvalue
530+
// is not closed false is returned and nothing is done, else returns true and
531+
// sets the upvalue.
532+
//
533+
// Any other functions that share this upvalue will also be affected!
523534
func (l *State) SetUpVal(f, i, v int) bool {
524535
fn, ok := l.get(f).(*function)
525-
if !ok || i >= len(fn.uvDefs) {
536+
if !ok || i >= len(fn.up) {
526537
return false
527538
}
528539

529-
fn.uvClosed[i] = true
530-
fn.upVals[i] = l.get(v)
540+
def := fn.up[i]
541+
if !def.closed {
542+
return false
543+
}
544+
def.val = l.get(v)
531545
return true
532546
}
533547

@@ -772,20 +786,29 @@ func (l *State) ListFunc(i int) {
772786

773787
// Execution
774788

789+
// Used to create the return values for the compiler API functions (nothing else!).
775790
func (l *State) asFunc(proto *funcProto, env *table) *function {
776791
f := &function{
777792
proto: *proto,
778-
uvDefs: proto.upVals,
779-
uvClosed: make([]bool, len(proto.upVals)),
780-
upVals: make([]value, len(proto.upVals)),
781-
uvAbsIdxs: make([]int, len(proto.upVals)),
793+
up: make([]*upValue, len(proto.upVals)),
782794
}
783-
for i := range f.uvAbsIdxs {
784-
f.uvAbsIdxs[i] = -1
795+
for i := range f.up {
796+
def := proto.upVals[i].makeUp()
797+
798+
// Don't set name or index! name may come in from debug info, index is meaningless when closed.
799+
def.closed = true
800+
def.absIdx = -1
801+
f.up[i] = def
785802
}
786803

787-
f.uvClosed[0] = true
788-
f.upVals[0] = env
804+
// Top level functions must have their first upvalue as _ENV
805+
if len(f.up) > 0 {
806+
if f.up[0].name != "_ENV" && f.up[0].name != "" {
807+
luautil.Raise("Top level function without _ENV or _ENV in improper position.", luautil.ErrTypGenRuntime)
808+
}
809+
810+
f.up[0].val = env
811+
}
789812

790813
return f
791814
}
@@ -848,7 +871,7 @@ func (l *State) LoadText(in io.Reader, name string, env int) error {
848871
// This version looks for and runs "luac" to compile the chunk. Make sure luac is on
849872
// your path.
850873
//
851-
// This functions is not safe for concurrent use.
874+
// This function is not safe for concurrent use.
852875
func (l *State) LoadTextExternal(in io.Reader, name string, env int) error {
853876
outFile := os.TempDir() + "/dctech.lua.bin" // Go seems to lack a function to get a temporary file name, so this is unsafe for concurrent use!
854877
cmd := exec.Command("luac", "-o", outFile, "-")
@@ -946,6 +969,10 @@ func (l *State) PCall(args, rtns int) (err error) {
946969
trace = fmt.Sprintf("%v\n\nNative Trace:\n%s\n", trace, buf)
947970
}
948971

972+
// Before we strip the stack we need to close all upvalues in the section we will be stripping, just in
973+
// case a closure was assigned to another upvalue.
974+
l.stack.frames[len(l.stack.frames) - 1].closeUpAbs(top)
975+
949976
// Make sure the stack is back to the way we found it, minus the function and it's arguments.
950977
l.stack.frames = l.stack.frames[:frames]
951978
for i := len(l.stack.data) - 1; i >= top; i-- {

ast/parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (p *parser) funcDeclStat(local bool) Stmt {
8888
p.l.getCurrent(tknName)
8989
ident = exprLine(&TableAccessor{
9090
Obj: ident,
91-
Key: exprLine(&ConstIdent{
91+
Key: exprLine(&ConstString{
9292
Value: p.l.current.Lexeme,
9393
}, p.l.current.Line),
9494
}, line)

0 commit comments

Comments
 (0)