diff --git a/doc/manual.md b/doc/manual.md index 3745012..fb7b06f 100644 --- a/doc/manual.md +++ b/doc/manual.md @@ -281,7 +281,7 @@ An `import` statement references another module and lets the current module use its exported module variables and functions. Its syntax is: - local = import "" + [local =] import "" The module name `` is a name like `foo`, `foo.bar`, or `foo.bar.baz`. The Titan compiler translates a module name into a file name by converting @@ -290,6 +290,9 @@ all dots to path separators, and then appending `.so`, for binary modules, or modules will correspond to `foo.{so|titan}`, `foo/bar.{so|titan}`, and `foo/bar/baz.{so|titan}`. The Titan compiler will recompile the module if its source is newer than its binary. +If the `` is not given, Titan will use the last name of +the module (`foo` for all of `foo`, `bar.foo`, or `baz.bar.foo`, for example). + Binary modules are looked up in the *runtime search path*, a semicolon-separated list of paths that defaults to `.;/usr/local/lib/titan/0.5`, but can be overriden with a `TITAN_PATH_0_5` or `TITAN_PATH` environment variable. Source modules are looked in @@ -323,12 +326,15 @@ as call the exported function `bar`. A `foreign import` statement loads a C header file, and imports all its function and type definitions. Its syntax is: - local = foreign import "" + [local =] foreign import "" The string containing the `` argument may be a relative or absolute pathname to a C header file. When a relative pathname is given, the Titan compiler will search in standard C header paths. +If the `` is not given, Titan will use the name of the header +file without the path part (`foo` for `path/foo.h`, for example). + Due to the flat namespace of C functions and types, all types and functions imported via foreign imports share a single namespace in Titan as well. @@ -340,8 +346,8 @@ many headers will import the same system headers. For example, if one loads two header files like this - local foo = foreign import "foo.h" - local bar = foreign import "bar.h" + foreign import "foo.h" + foreign import "bar.h" and both `foo.h` and `bar.h` happen to include (directly or indirectly) the system header `stdio.h`, this means that both `foo.printf` and `bar.printf` @@ -400,11 +406,63 @@ Casts from `float` to `integer` fail if it is not an integral value, and if this value is outside the allowable range for integers. Casts from `value` fail if the value does not have the target type, or cannot be converted to it. +## The standard library + +### Basic functions + +You can call these functions directly from anywhere in a Titan module, +without needing to import anything. + +#### assert(v: value, msg: string): string + +Returns `v` if it is truthy, otherwise throws an error with message `msg`. + +#### dofile(filename: string, ...: value): { value } + +Executes the Lua chunk inside the file named `filename`, passing any extra +arguments (you can access these arguments inside the Lua code with `...`). +Returns an array containing all the values that the Lua chunk returned. + +The Lua code can load (using `require`) any Titan module that is part of +a Titan application, if you are running in the context of an application. + +#### dostring(chunk: string, ...: value): { value } + +Executes the Lua chunk inside `chunk`, passing any extra +arguments (you can access these arguments inside the Lua code with `...`). +Returns an array containing all the values that the Lua chunk returned. + +The Lua code can load (using `require`) any Titan module that is part of +a Titan application, if you are running in the context of an application. + +#### error(msg: string) + +Throws an error with error message `msg`. + +#### print(...: value) + +Prints its arguments to the standard output, separated by tabs and ending in a newline +(`print()` prints just a newline). + +#### tostring(v: value): string + +Returns the string representation for the Titan value `v`. + +#### tofloat(s: string): float + +If `s` has a numeric literal returns the corresponding number as a floating +point value, otherwise returns `0.0`. + +#### tointeger(s: string): integer + +If `s` has an integer literal or an integral floating point literal +returns the corresponding integer value, otherwise returns `0`. + ## The Complete Syntax of Titan Here is the complete syntax of Titan in extended BNF. As usual in extended BNF, {A} means 0 or more As, and \[A\] means an optional A. - program ::= {tlfunc | tlvar | tlrecord | tlimport | tlforeignimport} + program ::= {tlfunc | tlvar | tlrecord | tlimport | tlforeignimport | tlforeignfunc} tlfunc ::= [local] function Name '(' [parlist] ')' ':' type block end @@ -412,11 +470,15 @@ Here is the complete syntax of Titan in extended BNF. As usual in extended BNF, tlrecord ::= record Name recordfields end - tlimport ::= local Name '=' import LiteralString + tlimport ::= [local Name '='] import LiteralString + + tlforeignimport ::= [local Name '='] foreign import LiteralString + + tlforeignfunc ::= foreign function Name '(' [parlist] ')' ':' type - tlforeignimport ::= local Name '=' foreign import LiteralString + param ::= Name ':' type | '...' ':' type - parlist ::= Name ':' type {',' Name ':' type} + parlist ::= param {',' param} type ::= value | integer | float | boolean | string | array | map diff --git a/spec/checker_spec.lua b/spec/checker_spec.lua index 3ca7409..bb70fa1 100644 --- a/spec/checker_spec.lua +++ b/spec/checker_spec.lua @@ -1629,25 +1629,29 @@ describe("Titan type checker", function() end) it("catches use of function as first-class value", function () - local code = [[ + assert_type_error("access a function", [[ function foo(): integer return foo end - ]] - local ok, err = run_checker(code) - assert.falsy(ok) - assert.match("access a function", err) + ]]) + assert_type_error("access a function", [[ + function foo(): integer + return print + end + ]]) end) it("catches assignment to function", function () - local code = [[ + assert_type_error("assign to a function",[[ function foo(): integer foo = 2 end - ]] - local ok, err = run_checker(code) - assert.falsy(ok) - assert.match("assign to a function", err) + ]]) + assert_type_error("assign to a function",[[ + function foo(): integer + print = 2 + end + ]]) end) it("catches use of external function as first-class value", function () @@ -1907,13 +1911,56 @@ describe("Titan type checker", function() function h() f(20, (g())) end - ]], args = 2, params = 3 }} + ]], args = 2, params = 3 },{ code = [[ + function g(): integer + return 20 + end + function h() + assert(g()) + end + ]], args = 1, params = 2 }, { code = [[ + function g(): (integer, integer) + return 20, 30 + end + function h() + assert(20, g()) + end + ]], args = 3, params = 2 }, { code = [[ + function g(): (integer, integer) + return 20, 30 + end + function h() + assert((g())) + end + ]], args = 1, params = 2 }} for _, c in ipairs(cases) do local ok, err = run_checker(c.code) assert.falsy(ok) - assert.match("function 'f' called with " .. c.args .. " arguments but expects " .. c.params, err) + assert.match("called with " .. c.args .. " arguments but expects " .. c.params, err) end end) + + it("vararg foreigns and functions", function () + assert_type_check([[ + function g() + print('foo', 1, 2.5) + end + ]]) + assert_type_error("only the last parameter", [[ + function f(...: float, a: string) + end + ]]) + assert_type_error("only the last parameter", [[ + record R end + function R:f(...: float, a: string) + end + ]]) + assert_type_error("expected string but found float", [[ + function g() + dostring(2.5, 1, 'bar') + end + ]]) + end) end) describe("Titan typecheck of records", function() diff --git a/spec/coder_spec.lua b/spec/coder_spec.lua index 587b392..2c52411 100644 --- a/spec/coder_spec.lua +++ b/spec/coder_spec.lua @@ -16,17 +16,19 @@ local function generate_modules(modules, main, forapp) types.registry = {} local imported = {} local loader = driver.tableloader(modules, imported) - local type, err = checker.checkimport(main, loader) - if not type then return nil, err end - if #err ~= 0 then return nil, table.concat(err, "\n") end - local ofiles = {} + for name, _ in pairs(modules) do + local type, err = checker.checkimport(name, loader) + if not type then return nil, err end + if #err ~= 0 then return nil, table.concat(err, "\n") end + end + local mods = {} for name, mod in pairs(imported) do local ok, err = driver.compile_module(name, mod, nil, forapp, verbose) - table.insert(ofiles, name .. ".o") + table.insert(mods, name) if not ok then return nil, err end end if forapp then - local ok, err = driver.compile_program(main, ofiles, nil, verbose) + local ok, err = driver.compile_program(main, mods, nil, verbose) if not ok then return nil, err end end return true @@ -36,9 +38,9 @@ local function call(modname, code) local cmd = string.format("lua-5.3.4/src/lua -l %s -e \"print(pcall(function () %s end))\"", modname, code) local f = io.popen(cmd) - local out = f:read() + local out = f:read("*a") local ok, err = f:close() - if not ok then return false, err end + if not ok then return false, err, out end local ok, data = out:match("^(true)%s*(.*)$") if not ok then local _, error = out:match("^(false)%s*(.*)$") @@ -56,7 +58,7 @@ local function run_coder(titan_code, lua_test, errmsg) assert.equal(0, #err, table.concat(err, "\n")) local ok, err = driver.compile("test", ast, nil, nil, nil, verbose) assert.truthy(ok, err) - local ok, err = call("test", lua_test) + local ok, err, out = call("test", lua_test) if errmsg then assert.falsy(ok) assert.match(errmsg, err) @@ -68,7 +70,7 @@ end local function call_app(appname, ...) local cmd = table.concat({ appname, ... }, " ") local f = io.popen(cmd) - local out = f:read() + local out = f:read("*a") local ok, err, status = f:close() if err == "exit" then ok = true end if not verbose then os.remove(appname) end @@ -89,7 +91,7 @@ local function run_coder_app(titan_code, main, estatus, eout) assert.equal(0, #err, table.concat(err, "\n")) local ok, err = driver.compile("test", ast, nil, nil, true, verbose) assert.truthy(ok, err) - local ok, err = driver.compile_program("test", { "test.o" }, nil, verbose) + local ok, err = driver.compile_program("test", { "test" }, nil, verbose) assert.truthy(ok, err) local ok, err, status, output = call_app("./test") assert.truthy(ok, err) @@ -2273,6 +2275,57 @@ describe("Titan code generator", function() assert.equal(0, status) end) + it("correctly uses module local variables in application", function () + local modules = { + foo = [[ + local xs: {integer} = {} + function resetxs() + xs = {} + end + ]], + bar = [[ + local foo = import "foo" + function main(args: {string}): integer + foo.resetxs() + return 42 + end + ]] + } + local ok, err = generate_modules(modules, "bar", true) + assert.truthy(ok, err) + local ok, err, status = call_app("./bar") + assert.truthy(ok, err) + assert.equal(42, status) + end) + + it("loads titan module from lua code inside application", function () + local modules = { + foo = [[ + function a(): integer + return 42 + end + ]], + bar = [[ + function main(args: {string}): integer + local res = dostring([=[ + local m = require 'foo' + return m.a() + ]=]) + if res[1] == 42 then + return 0 + else + return 1 + end + end + ]] + } + local ok, err = generate_modules(modules, "bar", true) + assert.truthy(ok, err) + local ok, err, status = call_app("./bar") + assert.truthy(ok, err) + assert.equal(0, status) + end) + it("correctly uses module local variables in application", function () local modules = { foo = [[ @@ -2295,8 +2348,117 @@ describe("Titan code generator", function() assert.truthy(ok, err) assert.equal(42, status) end) + end) + + describe("#foreigns", function() + it("call print", function () + run_coder_app([[ + ]], [[ + print(1, 'foo', true, 2.5) + print() + return 0 + ]], 0, "1\tfoo\ttrue\t2.5\n\n") + end) + + it("call assert", function () + run_coder([[ + function f(cond: value): value + return assert(cond, 'foo') + end + ]], [[ + assert(test.f(42) == 42) + local ok, err = pcall(test.f, nil) + assert(not ok) + assert(err:match('foo')) + local ok, err = pcall(test.f, false) + assert(not ok) + assert(err:match('foo')) + ]]) + end) + + it("call dostring", function () + run_coder([=[ + function f(): { value } + return dostring([[ + return ... + ]], 1, 'foo', 2.5) + end + ]=], [[ + local res = test.f() + assert(#res == 3) + assert(res[1] == 1) + assert(res[2] == 'foo') + assert(res[3] == 2.5) + ]]) + end) + + it("call dofile", function () + os.execute('echo "return ..." > test_dofile.lua') + run_coder([=[ + function f(): { value } + return dofile('test_dofile.lua', 1, 'foo', 2.5) + end + ]=], [[ + local res = test.f() + assert(#res == 3) + assert(res[1] == 1) + assert(res[2] == 'foo') + assert(res[3] == 2.5) + ]]) + os.remove("test_dofile.lua") + end) + + it("call error", function () + run_coder([[ + function f(): nil + error('foo') + end + ]], [[ + local ok, err = pcall(test.f) + assert(not ok) + assert(err:match('foo')) + ]]) + end) + + it("call tostring", function () + run_coder([[ + function f(v: value): string + return tostring(v) + end + ]], [[ + assert(test.f(1) == '1') + assert(test.f(2.5) == '2.5') + assert(test.f('foo') == 'foo') + assert(test.f(true) == 'true') + assert(test.f(nil) == 'nil') + ]]) + end) + + it("call tofloat", function () + run_coder([[ + function f(v: string): float + return tofloat(v) + end + ]], [[ + assert(test.f('1') == 1.0) + assert(test.f('2.5') == 2.5) + assert(test.f('foo') == 0.0) + ]]) + end) + it("call tointeger", function () + run_coder([[ + function f(v: string): integer + return tointeger(v) + end + ]], [[ + assert(test.f('10') == 10) + assert(test.f('2.5') == 0) + assert(test.f('foo') == 0) + ]]) + end) end) + end) diff --git a/spec/lexer_spec.lua b/spec/lexer_spec.lua index ce9a27b..9b24ebd 100644 --- a/spec/lexer_spec.lua +++ b/spec/lexer_spec.lua @@ -9,59 +9,43 @@ end -- Find out how the lexer lexes a given string. -- Produces an error message if the string cannot be lexed or if there -- are multiple ways to lex it. -local function run_lexer(source) +local function run_lexer(source, tokens) local all_matched_tokens = {} local all_captures = {} local i = 1 - while i <= #source do - - local found_token = nil - local found_captures = nil - local found_j = nil - - for tokname, tokpat in pairs(lexer) do - local a, b, c = lpeg.match(lpeg.Ct(tokpat) * lpeg.Cp(), source, i) - if a then - local captures, j = a, b - if i == j then - return string.format( - "error: token %s matched the empty string", - tokname) - elseif found_token then - return string.format( - "error: multiple matching tokens: %s %s", - found_token, tokname) - else - found_token = tokname - found_captures = captures - found_j = j - end - elseif b and b ~= 'fail' then - return { err = b } - end - end - if not found_token then + for _, tokname in ipairs(tokens) do + local a, b, c = lpeg.match(lpeg.Ct(lexer[tokname]) * lpeg.Cp(), source, i) + if a then + local captures, j = a, b + if i == j then + return string.format( + "error: token %s matched the empty string", + tokname) + else + table.insert(all_matched_tokens, tokname) + table_extend(all_captures, captures) + i = j + end + elseif b and b ~= 'fail' then + return { err = b } + else return "error: lexer got stuck" end - - table.insert(all_matched_tokens, found_token) - table_extend(all_captures, found_captures) - i = found_j end return { tokens = all_matched_tokens, captures = all_captures } end local function assert_lex(source, expected_tokens, expected_captures) - local lexed = run_lexer(source) + local lexed = run_lexer(source, expected_tokens) local expected = { tokens = expected_tokens, captures = expected_captures } assert.are.same(expected, lexed) end -local function assert_error(source, expected_error ) - local lexed = run_lexer(source) +local function assert_error(source, tokname, expected_error ) + local lexed = run_lexer(source, { tokname }) local expected = { err = expected_error } assert.are.same(expected, lexed) end @@ -136,19 +120,19 @@ describe("Titan lexer", function() end) it("rejects invalid numeric literals ", function() - assert_error("1abcdef", "MalformedNumber") - assert_error("1.2.3.4", "MalformedNumber") - assert_error("1e", "MalformedNumber") - assert_error("1e2e3", "MalformedNumber") - assert_error("1p5", "MalformedNumber") - assert_error(".1.", "MalformedNumber") - assert_error("4..", "MalformedNumber") + assert_error("1abcdef", "NUMBER", "MalformedNumber") + assert_error("1.2.3.4", "NUMBER", "MalformedNumber") + assert_error("1e", "NUMBER", "MalformedNumber") + assert_error("1e2e3", "NUMBER", "MalformedNumber") + assert_error("1p5", "NUMBER", "MalformedNumber") + assert_error(".1.", "NUMBER", "MalformedNumber") + assert_error("4..", "NUMBER", "MalformedNumber") -- This is actually accepted by Lua (!) - assert_error("local x = 1337require", "MalformedNumber") + assert_error("1337require", "NUMBER", "MalformedNumber") -- This is rejected by Lua ('c' is an hexdigit) - assert_error("local x = 1337collectgarbage", "MalformedNumber") + assert_error("1337collectgarbage", "NUMBER", "MalformedNumber") end) it("can lex some short strings", function() @@ -196,11 +180,11 @@ describe("Titan lexer", function() end) it("rejects invalid string escape sequences", function() - assert_error([["\o"]], "InvalidEscape") + assert_error([["\o"]], "STRINGLIT", "InvalidEscape") end) it("rejects invalid decimal escapes", function() - assert_error([["\555"]], "MalformedEscape_decimal") + assert_error([["\555"]], "STRINGLIT", "MalformedEscape_decimal") end) it("allows digits after decimal escape", function () @@ -208,31 +192,31 @@ describe("Titan lexer", function() end) it("rejects invalid hexadecimal escapes", function() - assert_error([["\x"]], "MalformedEscape_x") - assert_error([["\xa"]], "MalformedEscape_x") - assert_error([["\xag"]], "MalformedEscape_x") + assert_error([["\x"]], "STRINGLIT", "MalformedEscape_x") + assert_error([["\xa"]], "STRINGLIT", "MalformedEscape_x") + assert_error([["\xag"]], "STRINGLIT", "MalformedEscape_x") end) it("rejects invalid unicode escapes", function() - assert_error([["\u"]], "MalformedEscape_u") - assert_error([["\u{"]], "MalformedEscape_u") - assert_error([["\u{ab1"]], "MalformedEscape_u") - assert_error([["\u{ag}"]], "MalformedEscape_u") + assert_error([["\u"]], "STRINGLIT", "MalformedEscape_u") + assert_error([["\u{"]], "STRINGLIT", "MalformedEscape_u") + assert_error([["\u{ab1"]], "STRINGLIT", "MalformedEscape_u") + assert_error([["\u{ag}"]], "STRINGLIT", "MalformedEscape_u") end) it("rejects unclosed strings", function() - assert_error('"\'', "UnclosedShortString") - assert_error('"A', "UnclosedShortString") + assert_error('"\'', "STRINGLIT", "UnclosedShortString") + assert_error('"A', "STRINGLIT", "UnclosedShortString") - assert_error('"A\n', "UnclosedShortString") - assert_error('"A\r', "UnclosedShortString") - assert_error('"A\\\n\nB', "UnclosedShortString") - assert_error('"A\\\r\rB', "UnclosedShortString") + assert_error('"A\n', "STRINGLIT", "UnclosedShortString") + assert_error('"A\r', "STRINGLIT", "UnclosedShortString") + assert_error('"A\\\n\nB', "STRINGLIT", "UnclosedShortString") + assert_error('"A\\\r\rB', "STRINGLIT", "UnclosedShortString") - assert_error('"\\"', "UnclosedShortString") + assert_error('"\\"', "STRINGLIT", "UnclosedShortString") - assert_error("[[]", "UnclosedLongString") - assert_error("[[]=]", "UnclosedLongString") + assert_error("[[]", "STRINGLIT", "UnclosedLongString") + assert_error("[[]=]", "STRINGLIT", "UnclosedLongString") end) it("can lex some long strings", function() diff --git a/spec/parser_spec.lua b/spec/parser_spec.lua index 1a1bc1d..80ce383 100644 --- a/spec/parser_spec.lua +++ b/spec/parser_spec.lua @@ -702,6 +702,27 @@ describe("Titan parser", function() assert_program_ast([[ local foo = import "module.foo" ]], { { _tag = "Ast.TopLevelImport", localname = "foo", modname = "module.foo" }, }) + assert_program_ast([[ import "module.foo" ]], { + { _tag = "Ast.TopLevelImport", localname = "foo", modname = "module.foo" }, + }) + assert_program_ast([[ local foo = import "foo" ]], { + { _tag = "Ast.TopLevelImport", localname = "foo", modname = "foo" }, + }) + assert_program_ast([[ import "foo" ]], { + { _tag = "Ast.TopLevelImport", localname = "foo", modname = "foo" }, + }) + assert_program_ast([[ local foo = foreign import "module/foo.h" ]], { + { _tag = "Ast.TopLevelForeignImport", localname = "foo", headername = "module/foo.h" }, + }) + assert_program_ast([[ local foo = foreign import "foo.h" ]], { + { _tag = "Ast.TopLevelForeignImport", localname = "foo", headername = "foo.h" }, + }) + assert_program_ast([[ foreign import "module/foo.h" ]], { + { _tag = "Ast.TopLevelForeignImport", localname = "foo", headername = "module/foo.h" }, + }) + assert_program_ast([[ foreign import "foo.h" ]], { + { _tag = "Ast.TopLevelForeignImport", localname = "foo", headername = "foo.h" }, + }) end) it("can parse multiple assignment", function () @@ -870,21 +891,33 @@ describe("Titan parser", function() assert_statements_syntax_error([[ (x) = 42 ]], "ExpAssign") end) - it("does not allow identifiers that are type names", function() - assert_program_syntax_error([[ + it("allows identifiers that are type names only outside type context", function() + assert_parses_successfuly([[ function integer() end - ]], "NameFunc") + ]]) - assert_program_syntax_error([[ + assert_parses_successfuly([[ function f() local integer: integer = 10 end - ]], "DeclLocal") + ]]) + + assert_program_syntax_error([[ + function f() + local integer: foo.integer = 10 + end + ]], "QualName") + + assert_parses_successfuly([[ + function f() + local integer: integer.foo = 10 + end + ]]) end) - it("doesn't allow using a primitive type as a record", function() - assert_expression_syntax_error("integer.new(10)", "ExpAssign") + it("allows using a primitive type as a module name", function() + assert_expression_ast("integer.new(10)", {}) end) it("uses specific error labels for some errors", function() @@ -1150,5 +1183,6 @@ describe("Titan parser", function() assert_expression_syntax_error([[ y{42,,} ]], "ExpFieldList") assert_expression_syntax_error([[ foo as ]], "CastMissingType") + end) end) diff --git a/titan-compiler/ast.lua b/titan-compiler/ast.lua index 7413bd3..5f765c0 100644 --- a/titan-compiler/ast.lua +++ b/titan-compiler/ast.lua @@ -23,10 +23,12 @@ return typedecl("Ast", { TopLevelRecord = {"loc", "name", "fields"}, TopLevelImport = {"loc", "localname", "modname"}, TopLevelForeignImport = {"loc", "localname", "headername"}, + TopLevelForeignFunc = {"loc", "name", "params", "rettypes"}, }, Decl = { Decl = {"loc", "name", "type"}, + Vararg = {"loc", "type"} }, Stat = { diff --git a/titan-compiler/checker.lua b/titan-compiler/checker.lua index 910f675..1f7fdd8 100644 --- a/titan-compiler/checker.lua +++ b/titan-compiler/checker.lua @@ -1,5 +1,6 @@ local checker = {} +local parser = require "titan-compiler.parser" local location = require "titan-compiler.location" local symtab = require "titan-compiler.symtab" local types = require "titan-compiler.types" @@ -104,9 +105,6 @@ typefromnode = util.make_visitor({ end, ["Ast.TypeFunction"] = function(node, st, errors) - if #node.argtypes ~= 1 then - error("functions with 0 or 2+ return values are not yet implemented") - end local ptypes = {} for _, ptype in ipairs(node.argtypes) do table.insert(ptypes, typefromnode(ptype, st, errors)) @@ -333,7 +331,7 @@ checkstat = util.make_visitor({ local texp = var._type if texp._tag == "Type.Module" then checker.typeerror(errors, var.loc, "trying to assign to a module") - elseif texp._tag == "Type.Function" then + elseif texp._tag == "Type.Function" or texp._tag == "Type.ForeignFunction" then checker.typeerror(errors, var.loc, "trying to assign to a function") else -- mark this declared variable as assigned to @@ -530,11 +528,12 @@ local function checkargs(ftype, args, loc, fname, st, errors) local nparams = #ftype.params local nargs = #args local lastarg = args[nargs] + local vatype = (ftype.vararg and type(ftype.vararg) == "table") and ftype.vararg or nil for i = 1, nargs do local arg = args[i] local ptype = ftype.params[i] checkexp(arg, st, errors, ptype) - ptype = ptype or arg._type + ptype = ptype or vatype or arg._type args[i] = trycoerce(args[i], ptype, errors) local atype = args[i]._type checkmatch("argument " .. i .. " of call to " .. fname, ptype, atype, errors, arg.loc) @@ -769,7 +768,7 @@ checkexp = util.make_visitor({ "trying to access module '%s' as a first-class value", node.var.name) node._type = types.Invalid() - elseif texp._tag == "Type.Function" then + elseif texp._tag == "Type.Function" or texp._tag == "Type.ForeignFunction" then checker.typeerror(errors, node.loc, "trying to access a function as a first-class value") node._type = types.Invalid() @@ -1032,13 +1031,15 @@ checkexp = util.make_visitor({ local ftype = node.exp._type local fname = expname(node.exp) local var = node.exp.var - if not (var and var._decl and (var._decl._tag == "Ast.TopLevelFunc" or + if not (var and var._decl and + (var._decl._tag == "Ast.TopLevelForeignFunc" or + var._decl._tag == "Ast.TopLevelFunc" or var._decl._tag == "Type.ModuleMember" or var._decl._tag == "Type.StaticMethod")) then checker.typeerror(errors, node.loc, "first-class functions are not supported in this version of Titan") end - if ftype._tag ~= "Type.Function" then + if ftype._tag ~= "Type.Function" and ftype._tag ~= "Type.ForeignFunction" then checker.typeerror(errors, node.loc, "'%s' is not a function but %s", fname, types.tostring(ftype)) @@ -1131,6 +1132,7 @@ local function checkfunc(node, st, errors) checktype(rettype, node.rettypes[i].loc, errors) end for i, param in ipairs(node.params) do + param.name = param.name or "..." st:add_symbol(param.name, param) param._type = ptypes[i] checktype(param._type, param.loc, errors) @@ -1254,7 +1256,8 @@ local function toplevel_name(node) elseif tag == "Ast.TopLevelVar" then return node.decl.name elseif tag == "Ast.TopLevelFunc" or - tag == "Ast.TopLevelRecord" then + tag == "Ast.TopLevelRecord" or + tag == "Ast.TopLevelForeignFunc" then return node.name elseif tag == "Ast.TopLevelMethod" or tag == "Ast.TopLevelStatic" then return nil @@ -1324,7 +1327,16 @@ local toplevel_visitor = util.make_visitor({ ["Ast.TopLevelFunc"] = function(node, st, errors) local ptypes = {} - for _, pdecl in ipairs(node.params) do + for i, pdecl in ipairs(node.params) do + if pdecl._tag == "Ast.Vararg" then + if i < #node.params then + checker.typeerror(errors, pdecl.loc, + "only the last parameter can be '...'") + else + checker.typeerror(errors, pdecl.loc, + "top-level functions cannot be variadic") + end + end table.insert(ptypes, typefromnode(pdecl.type, st, errors)) end local rettypes = {} @@ -1397,7 +1409,16 @@ local toplevel_visitor = util.make_visitor({ end node._record = record local ptypes = {} - for _, pdecl in ipairs(node.params) do + for i, pdecl in ipairs(node.params) do + if pdecl._tag == "Ast.Vararg" then + if i < #node.params then + checker.typeerror(errors, pdecl.loc, + "only the last parameter can be '...''") + else + checker.typeerror(errors, pdecl.loc, + "top-level functions cannot be variadic") + end + end table.insert(ptypes, typefromnode(pdecl.type, st, errors)) end local rettypes = {} @@ -1449,7 +1470,16 @@ local toplevel_visitor = util.make_visitor({ end node._record = record local ptypes = {} - for _, pdecl in ipairs(node.params) do + for i, pdecl in ipairs(node.params) do + if pdecl._tag == "Ast.Vararg" then + if i < #node.params then + checker.typeerror(errors, pdecl.loc, + "only the last parameter can be '...''") + else + checker.typeerror(errors, pdecl.loc, + "top-level functions cannot be variadic") + end + end table.insert(ptypes, typefromnode(pdecl.type, st, errors)) end local rettypes = {} @@ -1459,6 +1489,32 @@ local toplevel_visitor = util.make_visitor({ node._type = types.Method(rectype.name, node.name, ptypes, rettypes) rectype.methods[node.name] = node._type end, + + ["Ast.TopLevelForeignFunc"] = function(node, st, errors) + local ptypes = {} + local vararg = false + for i, pdecl in ipairs(node.params) do + if(pdecl._tag == "Ast.Vararg") then + if i < #node.params then + checker.typeerror(errors, pdecl.loc, + "only the last parameter can be '...'") + table.insert(ptypes, typefromnode(pdecl.type, st, errors)) + else + vararg = typefromnode(pdecl.type, st, errors) + end + else + table.insert(ptypes, typefromnode(pdecl.type, st, errors)) + end + end + local rettypes = {} + for _, rt in ipairs(node.rettypes) do + table.insert(rettypes, typefromnode(rt, st, errors)) + end + local fname = st and st.modname .. "." .. node.name or node.name + node._type = types.ForeignFunction(fname, + ptypes, rettypes, vararg) + return node + end, }) -- Colect type information of toplevel nodes @@ -1523,7 +1579,7 @@ local function makemoduletype(modname, modast) local tag = tlnode._tag if tag == "Ast.TopLevelVar" then table.insert(members, types.ModuleVariable(modname, tlnode.decl.name, tlnode._type)) - elseif tag == "Ast.TopLevelFunc" or tag == "Ast.TopLevelRecord" then + elseif tag == "Ast.TopLevelFunc" or tag == "Ast.TopLevelRecord" or tag == "Ast.TopLevelBuiltin" then table.insert(members, types.ModuleMember(modname, tlnode.name, tlnode._type)) end end @@ -1531,6 +1587,21 @@ local function makemoduletype(modname, modast) return types.Module(modname, members) end +local function add_foreigns(st) + local nodes, err, foo = parser.parse_foreign([[ + foreign function print(...: value) + foreign function assert(val: value, msg: string): value + foreign function dofile(fname: string, ...: value): {value} + foreign function dostring(fname: string, ...: value): {value} + foreign function error(msg: string) + foreign function tostring(val: value): string + foreign function tofloat(val: string): float + foreign function tointeger(val: string): integer + ]]) + for _, node in ipairs(nodes) do + st:add_symbol(node.name, toplevel_visitor(node)) + end +end -- Entry point for the typechecker -- ast: AST for the whole module @@ -1548,6 +1619,7 @@ function checker.check(modname, ast, subject, filename, loader) return nil, "you must pass a loader to import modules" end local st = symtab.new(modname) + add_foreigns(st) local errors = {subject = subject, filename = filename} checktoplevel(ast, st, errors, loader) checkbodies(ast, st, errors) diff --git a/titan-compiler/coder.lua b/titan-compiler/coder.lua index b48b1bf..6a24cef 100644 --- a/titan-compiler/coder.lua +++ b/titan-compiler/coder.lua @@ -114,6 +114,10 @@ local function func_name(modname, name) return modprefix(modname) .. name .. "_titan" end +local function foreign_name(name) + return "titan_" .. mangle_qn(name) +end + local function func_luaname(modname, name) return modprefix(modname) .. name .. "_lua" end @@ -419,6 +423,10 @@ local function function_sig(fname, ftype, is_pointer) for i = 2, #ftype.rettypes do table.insert(params, ctype(ftype.rettypes[i]) .. "*") end + if type(ftype.vararg) == "table" then + table.insert(params, "int") + table.insert(params, "...") + end local rettype = ftype.rettypes[1] local template = is_pointer and "$RETTYPE (*$FNAME)($PARAMS)" @@ -1060,9 +1068,19 @@ local function codecall(ctx, node) local fname local fnode = node.exp.var local modarg = "_mod" + local ftype = fnode._type if node.args._tag == "Ast.ArgsFunc" then if fnode._tag == "Ast.VarName" then - fname = func_name(ctx.module, fnode.name) + if ftype._tag == "Type.ForeignFunction" then + fname = foreign_name(ftype.name) + ctx.functions[ftype.name] = { + mangled = foreign_name(ftype.name), + type = ftype + } + else + assert(ftype._tag == "Type.Function") + fname = func_name(ctx.module, fnode.name) + end elseif fnode._tag == "Ast.VarDot" then if fnode.exp._type._tag == "Type.ForeignModule" then return codeforeigncall(ctx, node) @@ -1082,6 +1100,12 @@ local function codecall(ctx, node) INDEX = c_integer_literal(fnode.exp.var.exp.var._decl._upvalue) -- whew! }) end + elseif ftype._tag == "Type.ForeignFunction" then + fname = foreign_name(ftype.name) + ctx.functions[ftype.name] = { + mangled = foreign_name(ftype.name), + type = ftype + } else assert(fnode._decl._tag == "Type.ModuleMember") fname = func_name(fnode._decl.modname, fnode.name) @@ -1103,6 +1127,7 @@ local function codecall(ctx, node) else assert(node.args._tag == "Ast.ArgsMethod") local mtype = node._method + ftype = mtype fname = method_name(mtype) local mod, record = mtype.fqtn:match("^(.*)%.(%a+)$") if mod ~= ctx.module then @@ -1125,7 +1150,8 @@ local function codecall(ctx, node) table.insert(caexps, modarg) table.insert(caexps, cexp) end - for _, arg in ipairs(node.args.args) do + for i = 1, #ftype.params do + local arg = node.args.args[i] local cstat, cexp = codeexp(ctx, arg) table.insert(castats, cstat) table.insert(caexps, cexp) @@ -1140,6 +1166,15 @@ local function codecall(ctx, node) table.insert(caexps, "&" .. tmpname) retslots[i] = tmpslot end + if type(ftype.vararg) == "table" then + table.insert(caexps, c_integer_literal(#node.args.args - #ftype.params)) + end + for i = #ftype.params+1, #node.args.args do + local arg = node.args.args[i] + local cstat, cexp = codeexp(ctx, arg) + table.insert(castats, cstat) + table.insert(caexps, cexp) + end local cstats = table.concat(castats, "\n") local ccall = render("$NAME($CAEXPS)", { NAME = fname, @@ -2523,20 +2558,24 @@ local function init_data_from_other_modules(tlcontext, includes, loadmods, initm for name, func in pairs(tlcontext.functions) do local fname = func.mangled - table.insert(includes, storage_class .. " " .. function_sig(fname, func.type, is_dynamic) .. ";") - if is_dynamic then - local mprefix = mprefixes[func.module] - if not mprefix then - mprefix = mangle_qn(func.module) .. "_" - mprefixes[func.module] = mprefix - local file = func.module:gsub("[.]", "/") .. ".so" + if func.type._tag == "Type.ForeignFunction" then + table.insert(includes, "extern " .. function_sig(fname, func.type) .. ";") + else + table.insert(includes, storage_class .. " " .. function_sig(fname, func.type, is_dynamic) .. ";") + if is_dynamic then + local mprefix = mprefixes[func.module] + if not mprefix then + mprefix = mangle_qn(func.module) .. "_" + mprefixes[func.module] = mprefix + local file = func.module:gsub("[.]", "/") .. ".so" + table.insert(loadmods, render([[ + void *$HANDLE = loadlib(L, "$FILE"); + ]], { HANDLE = mprefix .. "handle", FILE = file })) + end table.insert(loadmods, render([[ - void *$HANDLE = loadlib(L, "$FILE"); - ]], { HANDLE = mprefix .. "handle", FILE = file })) + $NAME = cast_func($TYPE, loadsym(L, $HANDLE, "$NAME")); + ]], { NAME = fname, TYPE = function_sig("", func.type, true), HANDLE = mprefix .. "handle" })) end - table.insert(loadmods, render([[ - $NAME = cast_func($TYPE, loadsym(L, $HANDLE, "$NAME")); - ]], { NAME = fname, TYPE = function_sig("", func.type, true), HANDLE = mprefix .. "handle" })) end end diff --git a/titan-compiler/driver.lua b/titan-compiler/driver.lua index a645bd6..3765685 100644 --- a/titan-compiler/driver.lua +++ b/titan-compiler/driver.lua @@ -240,7 +240,7 @@ static int msghandler (lua_State *L) { return 1; } -int $LUAOPEN_FN(lua_State* L); +$OPENFN_SIGS /* main function, based on srlua */ int main(int argc, char** argv) { @@ -257,8 +257,7 @@ int main(int argc, char** argv) { luaL_openlibs(L); lua_getglobal(L, "package"); lua_getfield(L, -1, "preload"); - lua_pushcfunction(L, $LUAOPEN_FN); - lua_setfield(L, -2, "$MODNAME"); + $OPENFNS lua_pushcfunction(L, msghandler); lua_pushcfunction(L, pmain); lua_pushinteger(L, argc); @@ -273,13 +272,27 @@ int main(int argc, char** argv) { ]] -local function write_entrypoint(modname) +local function write_entrypoint(modname, modules) local entrypoint_c = modname:gsub("[.]", "/") .. "__entrypoint.c" local fd, err = io.open(entrypoint_c, "w") if not fd then return nil, err end + local openfns, openfn_sigs = {}, {} + for _, mod in ipairs(modules) do + table.insert(openfns, util.render([[ + lua_pushcfunction(L, $LUAOPEN_FN); + lua_setfield(L, -2, "$MODNAME"); + ]], { + LUAOPEN_FN = "luaopen_" .. mod:gsub("[.]", "_"), + MODNAME = mod, + })) + table.insert(openfn_sigs, util.render([[ + int $LUAOPEN_FN(lua_State* L); + ]], { LUAOPEN_FN = "luaopen_" .. mod:gsub("[.]", "_") })) + end local ok, err = fd:write(util.render(entrypoint_template, { PROGNAME = modname:gsub(".*[.]", ""), - LUAOPEN_FN = "luaopen_" .. modname:gsub("[.]", "_"), + OPENFNS = table.concat(openfns, "\n"), + OPENFN_SIGS = table.concat(openfn_sigs, "\n"), MODNAME = modname, })) if err then return nil, err end @@ -287,18 +300,18 @@ local function write_entrypoint(modname) return entrypoint_c end -function driver.compile_program(modname, libnames, link, verbose) +function driver.compile_program(modname, modules, link, verbose) local execname = modname:gsub("[.]", "/") os.remove(execname) - local entrypoint_c, err = write_entrypoint(modname) + local entrypoint_c, err = write_entrypoint(modname, modules) if err then return nil, err end local args = {driver.CC, driver.CFLAGS, "-o", execname, "-I", driver.TITAN_RUNTIME_PATH, "-I", driver.LUA_SOURCE_PATH, entrypoint_c} - for _, libname in ipairs(libnames) do - table.insert(args, libname) + for _, mod in ipairs(modules) do + table.insert(args, mod:gsub("[.]", "/") .. ".o") end table.insert(args, driver.TITAN_RUNTIME_PATH .. "titan.o") diff --git a/titan-compiler/lexer.lua b/titan-compiler/lexer.lua index 95f8616..a645af9 100644 --- a/titan-compiler/lexer.lua +++ b/titan-compiler/lexer.lua @@ -165,8 +165,10 @@ local keywords = { "and", "break", "do", "else", "elseif", "end", "for", "false", "function", "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while", "import", - "record", "as", "foreign", + "record", "as", "foreign" +} +local type_keywords = { "boolean", "integer", "float", "string", "value" } @@ -174,9 +176,20 @@ for _, keyword in ipairs(keywords) do lexer[keyword:upper()] = P(keyword) * -idrest end +for _, keyword in ipairs(type_keywords) do + lexer[keyword:upper()] = P(keyword) * -idrest +end + local is_keyword = {} +local is_type_keyword = {} + for _, keyword in ipairs(keywords) do is_keyword[keyword] = true + is_type_keyword[keyword] = true +end + +for _, keyword in ipairs(type_keywords) do + is_type_keyword[keyword] = true end lexer.NAME = Cmt(C(possiblename), function(_, pos, s) @@ -187,6 +200,14 @@ lexer.NAME = Cmt(C(possiblename), function(_, pos, s) end end) +lexer.TYPENAME = Cmt(C(possiblename), function(_, pos, s) + if not is_type_keyword[s] then + return pos, s + else + return false + end +end) + -- -- Symbolic tokens -- diff --git a/titan-compiler/parser.lua b/titan-compiler/parser.lua index 3c2576e..ea9cd67 100644 --- a/titan-compiler/parser.lua +++ b/titan-compiler/parser.lua @@ -220,6 +220,106 @@ function defs.recorddecl(pos, name, fields) ast.TopLevelStatic(pos, name, "new", params, { ast.TypeName(pos, name) }, body) end +function defs.shortimport(pos, name) + return ast.TopLevelImport(pos, name:match("(%w+)$"), name) +end + +function defs.shortforeign(pos, name) + return ast.TopLevelForeignImport(pos, name:match("(%w+)[.]h$"), name) +end + +local tokens = [[ + -- + -- Get current position + -- + + P <- {} => get_loc + + -- Create new rules for all our tokens, for the whitespace-skipping magic + -- Currently done by hand but this is something that parser-gen should be + -- able to do for us. + + SKIP <- (%SPACE / %COMMENT) + + AND <- %AND SKIP* + BREAK <- %BREAK SKIP* + DO <- %DO SKIP* + ELSE <- %ELSE SKIP* + ELSEIF <- %ELSEIF SKIP* + END <- %END SKIP* + FALSE <- %FALSE SKIP* + FOR <- %FOR SKIP* + FUNCTION <- %FUNCTION SKIP* + GOTO <- %GOTO SKIP* + IF <- %IF SKIP* + IN <- %IN SKIP* + LOCAL <- %LOCAL SKIP* + NIL <- %NIL SKIP* + NOT <- %NOT SKIP* + OR <- %OR SKIP* + RECORD <- %RECORD SKIP* + REPEAT <- %REPEAT SKIP* + RETURN <- %RETURN SKIP* + THEN <- %THEN SKIP* + TRUE <- %TRUE SKIP* + UNTIL <- %UNTIL SKIP* + WHILE <- %WHILE SKIP* + IMPORT <- %IMPORT SKIP* + AS <- %AS SKIP* + FOREIGN <- %FOREIGN SKIP* + + BOOLEAN <- %BOOLEAN SKIP* + INTEGER <- %INTEGER SKIP* + FLOAT <- %FLOAT SKIP* + STRING <- %STRING SKIP* + VALUE <- %VALUE SKIP* + + ADD <- %ADD SKIP* + SUB <- %SUB SKIP* + MUL <- %MUL SKIP* + MOD <- %MOD SKIP* + DIV <- %DIV SKIP* + IDIV <- %IDIV SKIP* + POW <- %POW SKIP* + LEN <- %LEN SKIP* + BAND <- %BAND SKIP* + BXOR <- %BXOR SKIP* + BOR <- %BOR SKIP* + SHL <- %SHL SKIP* + SHR <- %SHR SKIP* + CONCAT <- %CONCAT SKIP* + EQ <- %EQ SKIP* + LT <- %LT SKIP* + GT <- %GT SKIP* + NE <- %NE SKIP* + LE <- %LE SKIP* + GE <- %GE SKIP* + ASSIGN <- %ASSIGN SKIP* + LPAREN <- %LPAREN SKIP* + RPAREN <- %RPAREN SKIP* + LBRACKET <- %LBRACKET SKIP* + RBRACKET <- %RBRACKET SKIP* + LCURLY <- %LCURLY SKIP* + RCURLY <- %RCURLY SKIP* + SEMICOLON <- %SEMICOLON SKIP* + COMMA <- %COMMA SKIP* + DOT <- %DOT SKIP* + DOTS <- %DOTS SKIP* + DBLCOLON <- %DBLCOLON SKIP* + COLON <- %COLON SKIP* + RARROW <- %RARROW SKIP* + + NUMBER <- %NUMBER SKIP* + STRINGLIT <- %STRINGLIT SKIP* + NAME <- %NAME SKIP* + TYPENAME <- %TYPENAME SKIP* + + -- Synonyms + + NEG <- SUB + BNEG <- BXOR +]] + local grammar = re.compile([[ program <- SKIP* @@ -227,8 +327,11 @@ local grammar = re.compile([[ / toplevelfunc / toplevelvar / toplevelrecord + -- / toplevelforeign / import - / foreign )* |} !. + / shortimport + / foreign + / shortforeign )* |} !. method <- (P FUNCTION NAME COLON NAME^NameMethod LPAREN^LParPList paramlist RPAREN^RParPList @@ -238,11 +341,15 @@ local grammar = re.compile([[ LPAREN^LParPList paramlist RPAREN^RParPList rettypeopt block END^EndFunc) -> TopLevelFunc + toplevelforeign <- (P FOREIGN !IMPORT FUNCTION^ForeignFunc NAME^NameFunc + LPAREN^LParPList paramlist RPAREN^RParPList + rettypeopt) -> TopLevelForeignFunc + toplevelvar <- (P localopt decl ASSIGN^AssignVar !(IMPORT / FOREIGN) exp^ExpVarDec) -> TopLevelVar - toplevelrecord <- (P RECORD NAME^NameRecord + toplevelrecord <- (P RECORD TYPENAME^NameRecord recordfields^FieldRecord END^EndRecord) -> recorddecl @@ -253,36 +360,46 @@ local grammar = re.compile([[ (LPAREN STRINGLIT^StringLParImport RPAREN^RParImport / STRINGLIT^StringImport)) -> TopLevelImport + shortimport <- (P !FOREIGN IMPORT + (LPAREN STRINGLIT^StringLParImport RPAREN^RParImport / + STRINGLIT^StringImport)) -> shortimport + foreign <- (P LOCAL NAME^NameImport ASSIGN^AssignImport FOREIGN IMPORT^ImportImport (LPAREN STRINGLIT^StringLParImport RPAREN^RParImport / STRINGLIT^StringImport)) -> TopLevelForeignImport + shortforeign <- (P FOREIGN IMPORT^ImportImport + (LPAREN STRINGLIT^StringLParImport RPAREN^RParImport / + STRINGLIT^StringImport)) -> shortforeign + rettypeopt <- (P (COLON rettype^TypeFunc)?) -> rettypeopt paramlist <- {| (param (COMMA param^DeclParList)*)? |} -- produces {Decl} param <- (P NAME COLON^ParamSemicolon type^TypeDecl) -> Decl + / (P DOTS COLON^ParamSemicolon + type^TypeDecl) -> Vararg decl <- (P NAME (COLON type^TypeDecl)? -> opt) -> Decl decllist <- {| decl (COMMA decl^DeclParList)* |} -- produces {Decl} - simpletype <- (P NIL) -> TypeNil + simpletype <- ((P NAME DOT TYPENAME^QualName) -> TypeQualName + / (P TYPENAME) -> TypeName + / (P NIL) -> TypeNil / (P BOOLEAN) -> TypeBoolean / (P INTEGER) -> TypeInteger / (P FLOAT) -> TypeFloat / (P STRING) -> TypeString / (P VALUE) -> TypeValue - / (P NAME DOT NAME^QualName) -> TypeQualName - / (P NAME) -> TypeName / (P LCURLY type^TypeType COLON type^TypeType RCURLY^RCurlyType) -> TypeMap / (P LCURLY type^TypeType - RCURLY^RCurlyType) -> TypeArray + RCURLY^RCurlyType) -> TypeArray) !DOT typelist <- ( LPAREN {| (type (COMMA type^TypelistType)*)? |} @@ -420,96 +537,58 @@ local grammar = re.compile([[ fieldsep <- SEMICOLON / COMMA - -- - -- Get current position - -- +]] .. tokens, defs) - P <- {} => get_loc +local toplevelforeign = re.compile([[ - -- Create new rules for all our tokens, for the whitespace-skipping magic - -- Currently done by hand but this is something that parser-gen should be - -- able to do for us. + declarations <- SKIP* {| toplevelforeign* |} !. - SKIP <- (%SPACE / %COMMENT) + toplevelforeign <- (P FOREIGN !IMPORT FUNCTION^ForeignFunc NAME^NameFunc + LPAREN^LParPList paramlist RPAREN^RParPList + rettypeopt) -> TopLevelForeignFunc - AND <- %AND SKIP* - BREAK <- %BREAK SKIP* - DO <- %DO SKIP* - ELSE <- %ELSE SKIP* - ELSEIF <- %ELSEIF SKIP* - END <- %END SKIP* - FALSE <- %FALSE SKIP* - FOR <- %FOR SKIP* - FUNCTION <- %FUNCTION SKIP* - GOTO <- %GOTO SKIP* - IF <- %IF SKIP* - IN <- %IN SKIP* - LOCAL <- %LOCAL SKIP* - NIL <- %NIL SKIP* - NOT <- %NOT SKIP* - OR <- %OR SKIP* - RECORD <- %RECORD SKIP* - REPEAT <- %REPEAT SKIP* - RETURN <- %RETURN SKIP* - THEN <- %THEN SKIP* - TRUE <- %TRUE SKIP* - UNTIL <- %UNTIL SKIP* - WHILE <- %WHILE SKIP* - IMPORT <- %IMPORT SKIP* - AS <- %AS SKIP* - FOREIGN <- %FOREIGN SKIP* + rettypeopt <- (P (COLON rettype^TypeFunc)?) -> rettypeopt - BOOLEAN <- %BOOLEAN SKIP* - INTEGER <- %INTEGER SKIP* - FLOAT <- %FLOAT SKIP* - STRING <- %STRING SKIP* - VALUE <- %VALUE SKIP* + paramlist <- {| (param (COMMA param^DeclParList)*)? |} -- produces {Decl} - ADD <- %ADD SKIP* - SUB <- %SUB SKIP* - MUL <- %MUL SKIP* - MOD <- %MOD SKIP* - DIV <- %DIV SKIP* - IDIV <- %IDIV SKIP* - POW <- %POW SKIP* - LEN <- %LEN SKIP* - BAND <- %BAND SKIP* - BXOR <- %BXOR SKIP* - BOR <- %BOR SKIP* - SHL <- %SHL SKIP* - SHR <- %SHR SKIP* - CONCAT <- %CONCAT SKIP* - EQ <- %EQ SKIP* - LT <- %LT SKIP* - GT <- %GT SKIP* - NE <- %NE SKIP* - LE <- %LE SKIP* - GE <- %GE SKIP* - ASSIGN <- %ASSIGN SKIP* - LPAREN <- %LPAREN SKIP* - RPAREN <- %RPAREN SKIP* - LBRACKET <- %LBRACKET SKIP* - RBRACKET <- %RBRACKET SKIP* - LCURLY <- %LCURLY SKIP* - RCURLY <- %RCURLY SKIP* - SEMICOLON <- %SEMICOLON SKIP* - COMMA <- %COMMA SKIP* - DOT <- %DOT SKIP* - DOTS <- %DOTS SKIP* - DBLCOLON <- %DBLCOLON SKIP* - COLON <- %COLON SKIP* - RARROW <- %RARROW SKIP* + param <- (P NAME COLON^ParamSemicolon + type^TypeDecl) -> Decl + / (P DOTS COLON^ParamSemicolon + type^TypeDecl) -> Vararg - NUMBER <- %NUMBER SKIP* - STRINGLIT <- %STRINGLIT SKIP* - NAME <- %NAME SKIP* + simpletype <- ((P NAME DOT TYPENAME^QualName) -> TypeQualName + / (P TYPENAME) -> TypeName + / (P NIL) -> TypeNil + / (P BOOLEAN) -> TypeBoolean + / (P INTEGER) -> TypeInteger + / (P FLOAT) -> TypeFloat + / (P STRING) -> TypeString + / (P VALUE) -> TypeValue + / (P LCURLY type^TypeType + COLON + type^TypeType + RCURLY^RCurlyType) -> TypeMap + / (P LCURLY type^TypeType + RCURLY^RCurlyType) -> TypeArray) !DOT - -- Synonyms + typelist <- ( LPAREN + {| (type (COMMA type^TypelistType)*)? |} + RPAREN^RParenTypelist ) -- produces {Type} - NEG <- SUB - BNEG <- BXOR + rettype <- {| (P typelist RARROW + rettype^TypeReturnTypes) -> TypeFunction |} + / {| (P {| simpletype |} RARROW + rettype^TypeReturnTypes) -> TypeFunction |} + / typelist + / {| simpletype |} -]], defs) + type <- (P typelist RARROW + rettype^TypeReturnTypes) -> TypeFunction + / (P {| simpletype |} RARROW + rettype^TypeReturnTypes) -> TypeFunction + / simpletype + +]] .. tokens, defs) function parser.parse(filename, input) -- Abort if someone calls this non-reentrant parser recursively @@ -528,6 +607,17 @@ function parser.parse(filename, input) end end +function parser.parse_foreign(input) + local ast, err, errpos = toplevelforeign:match(input) + + if ast then + return ast + else + local loc = location.from_pos(filename, input, errpos) + return false, { label = err, loc = loc } + end +end + function parser.error_to_string(err) local errmsg = syntax_errors.errors[err.label] return location.format_error(err.loc, "syntax error: %s", errmsg) diff --git a/titan-compiler/syntax_errors.lua b/titan-compiler/syntax_errors.lua index 799a59e..d4403c1 100644 --- a/titan-compiler/syntax_errors.lua +++ b/titan-compiler/syntax_errors.lua @@ -169,6 +169,7 @@ local errors = { NameMethod = "Expected a method name after the colon.", + ForeignFunc = "Expected a foreign function declaration.", } syntax_errors.errors = errors diff --git a/titan-compiler/types.lua b/titan-compiler/types.lua index f581d7a..6d64aed 100644 --- a/titan-compiler/types.lua +++ b/titan-compiler/types.lua @@ -20,6 +20,7 @@ local types = typedecl("Type", { ModuleMember = {"modname", "name", "type"}, ModuleVariable = {"modname", "name", "type"}, StaticMethod = {"fqtn", "name", "params", "rettypes"}, + ForeignFunction = {"name", "params", "rettypes", "vararg"}, } }) @@ -223,12 +224,15 @@ function types.tostring(t) end elseif tag == "Type.Typedef" then return t.name - elseif tag == "Type.Function" then - local out = {"function("} + elseif tag == "Type.Function" or tag == "Type.ForeignFunction" then + local out = {tag == "Type.ForeignFunction" and "foreign " or "", "function("} local ptypes = {} for _, param in ipairs(t.params) do table.insert(ptypes, types.tostring(param)) end + if(type(t.vararg) == "table") then + table.insert(ptypes, types.tostring(t.vararg) .. "...") + end table.insert(out, table.concat(ptypes, ", ")) table.insert(out, ")") local rtypes = {} @@ -347,6 +351,19 @@ function types.serialize(t) "},{" ..table.concat(functions, ",") .. "}" .. "," .. "{" .. table.concat(methods, ",") .. "}" .. ")" + elseif tag == "Type.ForeignFunction" then + local ptypes = {} + for _, pt in ipairs(t.params) do + table.insert(ptypes, types.serialize(pt)) + end + local rettypes = {} + for _, rt in ipairs(t.rettypes) do + table.insert(rettypes, types.serialize(rt)) + end + return "ForeignFunction('" .. t.name .. "'," .. + "{" .. table.concat(ptypes, ",") .. "}" .. "," .. + "{" .. table.concat(rettypes, ",") .. "}" .. "," .. + tostring(t.vararg) .. ")" elseif tag == "Type.Integer" then return "Integer()" elseif tag == "Type.Boolean" then return "Boolean()" elseif tag == "Type.String" then return "String()" diff --git a/titan-runtime/titan.c b/titan-runtime/titan.c index 0d66290..bf68dc7 100644 --- a/titan-runtime/titan.c +++ b/titan-runtime/titan.c @@ -204,3 +204,137 @@ const TValue *getgeneric (Table *t, const TValue *key) { } } } + +/* Builtin foreign functions */ +int titan_print(lua_State *L, CClosure *_mod, int nargs, ...) { + if(nargs > 0) { + lua_checkstack(L, 3); + TValue *top = L->top; + va_list args; + va_start(args, nargs); + for(int i = 0; i < nargs; i++) { + TValue val = va_arg(args, TValue); + setobj2s(L, L->top-1, &val); + size_t l; + const char *s = luaL_tolstring(L, -1, &l); + if(i > 0) lua_writestring("\t", 1); + lua_writestring(s, l); + L->top--; + } + va_end(args); + L->top = top; + } + lua_writeline(); + return 0; +} + +TValue titan_assert(lua_State *L, CClosure *_mod, TValue cond, TString *msg) { + if(l_isfalse(&cond)) { + luaL_error(L, getstr(msg)); + } + return cond; +} + +Table *titan_dofile(lua_State *L, CClosure *_mod, TString *fname, int nargs, ...) { + TValue *top = L->top; + lua_checkstack(L, 2 + nargs); + lua_createtable(L, 0, 0); + Table *tres = hvalue(L->top-1); + TValue *firstres = L->top; + if (luaL_loadfile(L, getstr(fname)) != LUA_OK) { + lua_error(L); + } + if(nargs > 0) { + va_list args; + va_start(args, nargs); + for(int i = 0; i < nargs; i++) { + TValue arg = va_arg(args, TValue); + L->top++; + setobj2s(L, L->top-1, &arg); + } + va_end(args); + } + lua_call(L, nargs, LUA_MULTRET); + for(TValue *res = firstres; res < L->top; res++) { + luaH_setint(L, tres, res - firstres + 1, res); + } + L->top = top; + return tres; +} + +int titan_error(lua_State *L, CClosure *_mod, TString *msg) { + return luaL_error(L, getstr(msg)); +} + +Table *titan_dostring(lua_State *L, CClosure *_mod, TString *code, int nargs, ...) { + TValue *top = L->top; + lua_checkstack(L, 2 + nargs); + lua_createtable(L, 0, 0); + Table *tres = hvalue(L->top-1); + TValue *firstres = L->top; + if (luaL_loadstring(L, getstr(code)) != LUA_OK) { + lua_error(L); + } + if(nargs > 0) { + va_list args; + va_start(args, nargs); + for(int i = 0; i < nargs; i++) { + TValue arg = va_arg(args, TValue); + L->top++; + setobj2s(L, L->top-1, &arg); + } + va_end(args); + } + lua_call(L, nargs, LUA_MULTRET); + for(TValue *res = firstres; res < L->top; res++) { + luaH_setint(L, tres, res - firstres + 1, res); + } + L->top = top; + return tres; +} + +TString *titan_tostring(lua_State *L, CClosure *_mod, TValue val) { + TValue *top = L->top; + lua_checkstack(L, 3); + L->top++; + setobj2s(L, L->top-1, &val); + size_t l; + luaL_tolstring(L, -1, &l); + TString *s = tsvalue(L->top-1); + L->top = top; + return s; +} + +lua_Number titan_tofloat(lua_State *L, CClosure *_mod, TString *s) { + TValue val; + if(luaO_str2num(getstr(s), &val)) { + return nvalue(&val); + } else { + return 0; + } +} + +lua_Integer titan_tointeger(lua_State *L, CClosure *_mod, TString *s) { + TValue val; + if(luaO_str2num(getstr(s), &val)) { + lua_Integer res; + if(luaV_tointeger(&val, &res, 0)) { + return res; + } else { + return 0; + } + } else { + return 0; + } +} + +lua_Integer titan_string_byte(lua_State *L, CClosure *_mod, TString *s, lua_Integer index) { + size_t len = tsslen(s); + if(index == 0) + return 0; + else if(index < 0) + index = index % len; + else + index = (index - 1) % len; + return getstr(s)[index]; +} diff --git a/titanc b/titanc index 9854cbd..3505312 100755 --- a/titanc +++ b/titanc @@ -49,18 +49,18 @@ end if #errors > 0 then exit(table.concat(errors, "\n")) end -local libnames = {} +local modnames = {} for name, mod in pairs(driver.imported) do - local ok, err, libname = driver.compile_module(name, mod, args.link, main_module ~= nil, args.verbose) + local ok, err = driver.compile_module(name, mod, args.link, main_module ~= nil, args.verbose) if not ok then table.insert(errors, err) else - table.insert(libnames, libname) + table.insert(modnames, name) end end if main_module then - driver.compile_program(main_module, libnames, args.link, args.verbose) + driver.compile_program(main_module, modnames, args.link, args.verbose) end if #errors > 0 then exit(table.concat(errors, "\n")) end