Implemented extern functions

This commit is contained in:
Eduard Urbach 2025-02-12 00:04:30 +01:00
parent 1083db6ab2
commit 3b66dae1d4
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
24 changed files with 304 additions and 169 deletions

View File

@ -1,3 +1,7 @@
extern user32 {
MessageBoxA(window *Any, text *Int8, title *Int8, type UInt)
}
main() {
title := "Title."
text := "Hi!"

View File

@ -4,18 +4,18 @@ in(buffer []Int8) -> Int {
return sys.read(0, buffer, len(buffer))
}
out(message []Int8) -> Int {
return sys.write(1, message, len(message))
out(buffer []Int8) -> Int {
return sys.write(1, buffer, len(buffer))
}
error(message []Int8) -> Int {
return sys.write(2, message, len(message))
error(buffer []Int8) -> Int {
return sys.write(2, buffer, len(buffer))
}
read(fd Int, buffer []Int8) -> Int {
return sys.read(fd, buffer, len(buffer))
}
write(fd Int, message []Int8) -> Int {
return sys.write(fd, message, len(message))
write(fd Int, buffer []Int8) -> Int {
return sys.write(fd, buffer, len(buffer))
}

View File

@ -1,9 +1,9 @@
read(fd Int, address *Any, length Int) -> Int {
return syscall(0, fd, address, length)
read(fd Int, buffer *Any, length Int) -> Int {
return syscall(0, fd, buffer, length)
}
write(fd Int, address *Any, length Int) -> Int {
return syscall(1, fd, address, length)
write(fd Int, buffer *Any, length Int) -> Int {
return syscall(1, fd, buffer, length)
}
open(path *Any, flags Int, mode Int) -> Int {

View File

@ -1,9 +1,9 @@
read(fd Int, address *Any, length Int) -> Int {
return syscall(0x2000003, fd, address, length)
read(fd Int, buffer *Any, length Int) -> Int {
return syscall(0x2000003, fd, buffer, length)
}
write(fd Int, address *Any, length Int) -> Int {
return syscall(0x2000004, fd, address, length)
write(fd Int, buffer *Any, length Int) -> Int {
return syscall(0x2000004, fd, buffer, length)
}
open(path *Any, flags Int, mode Int) -> Int {

View File

@ -1,10 +1,16 @@
read(fd Int, address *Any, length Int) -> Int {
kernel32.ReadFile(fd, address, length)
extern kernel32 {
GetStdHandle(handle Int) -> Int
WriteConsoleA(fd Int, buffer *Any, length Int, written *Int) -> Bool
ReadFile(fd Int, buffer *Any, length Int) -> Bool
}
read(fd Int, buffer *Any, length Int) -> Int {
kernel32.ReadFile(fd, buffer, length)
return length
}
write(fd Int, address *Any, length Int) -> Int {
write(fd Int, buffer *Any, length Int) -> Int {
fd = kernel32.GetStdHandle(-10 - fd)
kernel32.WriteConsoleA(fd, address, length, 0)
kernel32.WriteConsoleA(fd, buffer, length, 0)
return length
}

View File

@ -1,3 +1,8 @@
extern kernel32 {
VirtualAlloc(address Int, length Int, flags Int, protection Int)
VirtualFree(address *Any, length Int, type Int) -> Bool
}
mmap(address Int, length Int, protection Int, flags Int) -> *Any {
return kernel32.VirtualAlloc(address, length, flags, protection)
}

View File

@ -1,3 +1,7 @@
extern kernel32 {
ExitProcess(code UInt)
}
exit(code Int) {
kernel32.ExitProcess(code)
}

View File

@ -108,6 +108,10 @@ func CompileFunctions(functions map[string]*core.Function) {
wg := sync.WaitGroup{}
for _, function := range functions {
if function.IsExtern() {
continue
}
wg.Add(1)
go func() {

32
src/core/BeforeCall.go Normal file
View File

@ -0,0 +1,32 @@
package core
import (
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
"git.akyoto.dev/cli/q/src/types"
)
// BeforeCall loads the registers with the parameter values and checks that the types match with the function signature.
func (f *Function) BeforeCall(fn *Function, parameters []*expression.Expression, registers []cpu.Register) error {
for i := len(parameters) - 1; i >= 0; i-- {
typ, err := f.ExpressionToRegister(parameters[i], registers[i])
if err != nil {
return err
}
if !types.Is(typ, fn.Input[i].Type) {
_, expectsPointer := fn.Input[i].Type.(*types.Pointer)
if expectsPointer && parameters[i].Token.Kind == token.Number && parameters[i].Token.Text(f.File.Bytes) == "0" {
continue
}
return errors.New(&errors.TypeMismatch{Encountered: typ.Name(), Expected: fn.Input[i].Type.Name(), ParameterName: fn.Input[i].Name}, f.File, parameters[i].Token.Position)
}
}
return nil
}

View File

@ -5,7 +5,6 @@ import (
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
"git.akyoto.dev/cli/q/src/types"
"git.akyoto.dev/cli/q/src/x86"
)
@ -48,22 +47,13 @@ func (f *Function) CompileCall(root *expression.Expression) ([]types.Type, error
name = nameNode.Token.Text(f.File.Bytes)
}
if pkg == "kernel32" || pkg == "user32" || pkg == "gdi32" || pkg == "comctl32" {
parameters := root.Children[1:]
registers := x86.WindowsInputRegisters[:len(parameters)]
fn, exists = f.Functions[pkg+"."+name]
for i := len(parameters) - 1; i >= 0; i-- {
_, err := f.ExpressionToRegister(parameters[i], registers[i])
if !exists {
return nil, errors.New(&errors.UnknownFunction{Name: name}, f.File, nameNode.Token.Position)
}
if err != nil {
return nil, err
}
}
f.DLLs = f.DLLs.Append(pkg, name)
f.DLLCall(fmt.Sprintf("%s.%s", pkg, name))
return nil, nil
} else if pkg != f.File.Package {
if pkg != f.File.Package && !fn.IsExtern() {
if f.File.Imports == nil {
return nil, errors.New(&errors.UnknownPackage{Name: pkg}, f.File, pkgNode.Token.Position)
}
@ -77,36 +67,30 @@ func (f *Function) CompileCall(root *expression.Expression) ([]types.Type, error
imp.Used = true
}
fn, exists = f.Functions[pkg+"."+name]
if !exists {
return nil, errors.New(&errors.UnknownFunction{Name: name}, f.File, nameNode.Token.Position)
}
parameters := root.Children[1:]
if len(parameters) != len(fn.Input) {
return nil, errors.New(&errors.ParameterCountMismatch{Function: fn.Name, Count: len(parameters), ExpectedCount: len(fn.Input)}, f.File, nameNode.Token.End())
}
registers := f.CPU.Input[:len(parameters)]
for i := len(parameters) - 1; i >= 0; i-- {
typ, err := f.ExpressionToRegister(parameters[i], registers[i])
if fn.IsExtern() {
f.DLLs = f.DLLs.Append(pkg, name)
registers := x86.WindowsInputRegisters[:len(parameters)]
err := f.BeforeCall(fn, parameters, registers)
if err != nil {
return nil, err
}
if !types.Is(typ, fn.Input[i].Type) {
_, expectsPointer := fn.Input[i].Type.(*types.Pointer)
f.DLLCall(fmt.Sprintf("%s.%s", pkg, name))
return fn.OutputTypes, nil
}
if expectsPointer && parameters[i].Token.Kind == token.Number && parameters[i].Token.Text(f.File.Bytes) == "0" {
continue
}
registers := f.CPU.Input[:len(parameters)]
err := f.BeforeCall(fn, parameters, registers)
return nil, errors.New(&errors.TypeMismatch{Encountered: typ.Name(), Expected: fn.Input[i].Type.Name(), ParameterName: fn.Input[i].Name}, f.File, parameters[i].Token.Position)
}
if err != nil {
return nil, err
}
f.CallSafe(fn, registers)

6
src/core/IsExtern.go Normal file
View File

@ -0,0 +1,6 @@
package core
// IsExtern returns true if the function has no body.
func (f *Function) IsExtern() bool {
return f.Body == nil
}

View File

@ -19,6 +19,10 @@ func (f *Function) ResolveTypes() error {
return errors.New(&errors.UnknownType{Name: typeName}, f.File, param.tokens[1].Position)
}
if f.IsExtern() {
continue
}
uses := token.Count(f.Body, f.File.Bytes, token.Identifier, param.Name)
if uses == 0 && param.Name != "_" {
@ -38,7 +42,7 @@ func (f *Function) ResolveTypes() error {
param.Type = types.ByName(typeName, f.Package, f.Structs)
if param.Type == nil {
return errors.New(&errors.UnknownType{Name: typeName}, f.File, param.tokens[1].Position)
return errors.New(&errors.UnknownType{Name: typeName}, f.File, param.tokens[0].Position)
}
f.OutputTypes = append(f.OutputTypes, param.Type)

View File

@ -9,6 +9,7 @@ var (
ExpectedIfBeforeElse = &Base{"Expected an 'if' block before 'else'"}
ExpectedPackageName = &Base{"Expected package name"}
ExpectedStructName = &Base{"Expected struct name"}
ExpectedDLLName = &Base{"Expected DLL name"}
InvalidNumber = &Base{"Invalid number"}
InvalidExpression = &Base{"Invalid expression"}
InvalidRune = &Base{"Invalid rune"}

54
src/scanner/scanExtern.go Normal file
View File

@ -0,0 +1,54 @@
package scanner
import (
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token"
)
// scanExtern scans a block of external function declarations.
func (s *Scanner) scanExtern(file *fs.File, tokens token.List, i int) (int, error) {
i++
if tokens[i].Kind != token.Identifier {
return i, errors.New(errors.ExpectedDLLName, file, tokens[i].Position)
}
dllName := tokens[i].Text(file.Bytes)
i++
if tokens[i].Kind != token.BlockStart {
return i, errors.New(errors.MissingBlockStart, file, tokens[i].Position)
}
i++
closed := false
for i < len(tokens) {
if tokens[i].Kind == token.Identifier {
function, j, err := scanFunctionSignature(file, tokens, i, token.NewLine)
if err != nil {
return j, err
}
i = j
function.Package = dllName
function.UniqueName = dllName + "." + function.Name
s.functions <- function
}
if tokens[i].Kind == token.BlockEnd {
closed = true
break
}
i++
}
if !closed {
return i, errors.New(errors.MissingBlockEnd, file, tokens[i].Position)
}
return i, nil
}

View File

@ -37,6 +37,8 @@ func (s *Scanner) scanFile(path string, pkg string) error {
i, err = s.scanStruct(file, tokens, i)
case token.Identifier:
i, err = s.scanFunction(file, tokens, i)
case token.Extern:
i, err = s.scanExtern(file, tokens, i)
case token.EOF:
return nil
case token.Invalid:

View File

@ -1,7 +1,6 @@
package scanner
import (
"git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token"
@ -9,84 +8,17 @@ import (
// scanFunction scans a function.
func (s *Scanner) scanFunction(file *fs.File, tokens token.List, i int) (int, error) {
function, i, err := scanFunctionSignature(file, tokens, i, token.BlockStart)
if err != nil {
return i, err
}
var (
groupLevel = 0
blockLevel = 0
nameStart = i
paramsStart = -1
paramsEnd = -1
bodyStart = -1
typeStart = -1
typeEnd = -1
blockLevel = 0
bodyStart = -1
)
i++
// Function parameters
for i < len(tokens) {
if tokens[i].Kind == token.GroupStart {
groupLevel++
i++
if groupLevel == 1 {
paramsStart = i
}
continue
}
if tokens[i].Kind == token.GroupEnd {
groupLevel--
if groupLevel < 0 {
return i, errors.New(errors.MissingGroupStart, file, tokens[i].Position)
}
if groupLevel == 0 {
paramsEnd = i
i++
break
}
i++
continue
}
if tokens[i].Kind == token.Invalid {
return i, errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(file.Bytes)}, file, tokens[i].Position)
}
if tokens[i].Kind == token.EOF {
if groupLevel > 0 {
return i, errors.New(errors.MissingGroupEnd, file, tokens[i].Position)
}
if paramsStart == -1 {
return i, errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position)
}
return i, nil
}
if groupLevel > 0 {
i++
continue
}
return i, errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position)
}
// Return type
if i < len(tokens) && tokens[i].Kind == token.ReturnType {
typeStart = i + 1
for i < len(tokens) && tokens[i].Kind != token.BlockStart {
i++
}
typeEnd = i
}
// Function definition
for i < len(tokens) {
if tokens[i].Kind == token.ReturnType {
@ -144,47 +76,7 @@ func (s *Scanner) scanFunction(file *fs.File, tokens token.List, i int) (int, er
return i, errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position)
}
name := tokens[nameStart].Text(file.Bytes)
body := tokens[bodyStart:i]
function := core.NewFunction(file.Package, name, file, body)
if typeStart != -1 {
if tokens[typeStart].Kind == token.GroupStart && tokens[typeEnd-1].Kind == token.GroupEnd {
typeStart++
typeEnd--
}
outputTokens := tokens[typeStart:typeEnd]
err := outputTokens.Split(func(tokens token.List) error {
function.Output = append(function.Output, core.NewOutput(tokens))
return nil
})
if err != nil {
return i, err
}
}
parameters := tokens[paramsStart:paramsEnd]
err := parameters.Split(func(tokens token.List) error {
if len(tokens) == 0 {
return errors.New(errors.MissingParameter, file, parameters[0].Position)
}
if len(tokens) == 1 {
return errors.New(errors.MissingType, file, tokens[0].End())
}
function.Input = append(function.Input, core.NewInput(tokens))
return nil
})
if err != nil {
return i, err
}
function.Body = tokens[bodyStart:i]
s.functions <- function
i++
return i, nil

View File

@ -0,0 +1,125 @@
package scanner
import (
"git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/token"
)
// scanFunctionSignature scans a function declaration without the body.
func scanFunctionSignature(file *fs.File, tokens token.List, i int, delimiter token.Kind) (*core.Function, int, error) {
var (
groupLevel = 0
nameStart = i
paramsStart = -1
paramsEnd = -1
typeStart = -1
typeEnd = -1
)
i++
// Function parameters
for i < len(tokens) {
if tokens[i].Kind == token.GroupStart {
groupLevel++
i++
if groupLevel == 1 {
paramsStart = i
}
continue
}
if tokens[i].Kind == token.GroupEnd {
groupLevel--
if groupLevel < 0 {
return nil, i, errors.New(errors.MissingGroupStart, file, tokens[i].Position)
}
if groupLevel == 0 {
paramsEnd = i
i++
break
}
i++
continue
}
if tokens[i].Kind == token.Invalid {
return nil, i, errors.New(&errors.InvalidCharacter{Character: tokens[i].Text(file.Bytes)}, file, tokens[i].Position)
}
if tokens[i].Kind == token.EOF {
if groupLevel > 0 {
return nil, i, errors.New(errors.MissingGroupEnd, file, tokens[i].Position)
}
if paramsStart == -1 {
return nil, i, errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position)
}
return nil, i, nil
}
if groupLevel > 0 {
i++
continue
}
return nil, i, errors.New(errors.ExpectedFunctionParameters, file, tokens[i].Position)
}
// Return type
if i < len(tokens) && tokens[i].Kind == token.ReturnType {
typeStart = i + 1
for i < len(tokens) && tokens[i].Kind != delimiter {
i++
}
typeEnd = i
}
name := tokens[nameStart].Text(file.Bytes)
function := core.NewFunction(file.Package, name, file, nil)
if typeStart != -1 {
if tokens[typeStart].Kind == token.GroupStart && tokens[typeEnd-1].Kind == token.GroupEnd {
typeStart++
typeEnd--
}
outputTokens := tokens[typeStart:typeEnd]
err := outputTokens.Split(func(tokens token.List) error {
function.Output = append(function.Output, core.NewOutput(tokens))
return nil
})
if err != nil {
return nil, i, err
}
}
parameters := tokens[paramsStart:paramsEnd]
err := parameters.Split(func(tokens token.List) error {
if len(tokens) == 0 {
return errors.New(errors.MissingParameter, file, parameters[0].Position)
}
if len(tokens) == 1 {
return errors.New(errors.MissingType, file, tokens[0].End())
}
function.Input = append(function.Input, core.NewInput(tokens))
return nil
})
return function, i, err
}

View File

@ -66,6 +66,7 @@ const (
___KEYWORDS___ // <keywords>
Assert // assert
Else // else
Extern // extern
If // if
Import // import
Loop // loop

View File

@ -25,13 +25,14 @@ func TestFunction(t *testing.T) {
}
func TestKeyword(t *testing.T) {
tokens := token.Tokenize([]byte("assert if import else loop return struct switch"))
tokens := token.Tokenize([]byte("assert if import else extern loop return struct switch"))
expected := []token.Kind{
token.Assert,
token.If,
token.Import,
token.Else,
token.Extern,
token.Loop,
token.Return,
token.Struct,

View File

@ -19,6 +19,8 @@ func identifier(tokens List, buffer []byte, i Position) (List, Position) {
kind = If
case "else":
kind = Else
case "extern":
kind = Extern
case "import":
kind = Import
case "loop":

View File

@ -31,6 +31,8 @@ func ByName(name string, pkg string, structs map[string]*Struct) Type {
switch name {
case "Any":
return Any
case "Bool":
return Bool
case "Int":
return Int
case "Int64":
@ -47,6 +49,8 @@ func ByName(name string, pkg string, structs map[string]*Struct) Type {
return Float64
case "Float32":
return Float32
case "UInt":
return UInt
}
typ, exists := structs[pkg+"."+name]

View File

@ -13,6 +13,8 @@ var (
)
var (
Bool = Int
Int = Int64
Float = Float64
UInt = Int
)

View File

@ -0,0 +1 @@
extern {}

View File

@ -15,6 +15,7 @@ var errs = []struct {
ExpectedError error
}{
{"EmptySwitch.q", errors.EmptySwitch},
{"ExpectedDLLName.q", errors.ExpectedDLLName},
{"ExpectedFunctionDefinition.q", errors.ExpectedFunctionDefinition},
{"ExpectedFunctionParameters.q", errors.ExpectedFunctionParameters},
{"ExpectedIfBeforeElse.q", errors.ExpectedIfBeforeElse},