Skip to content
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

Args kwargs #9

Merged
merged 5 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21,904 changes: 21,512 additions & 392 deletions bootstrap/stage0.c

Large diffs are not rendered by default.

218 changes: 157 additions & 61 deletions compiler/passes/typechecker.oc
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,11 @@ def TypeChecker::check_enum_constructor(&this, node: &AST): &Type {
let enom = variant.parent

let num_expected_fields = variant.num_fields()
let params = Vector<&Variable>::new()
for let i = 0; i < num_expected_fields; i += 1 {
let field = variant.get_field(i)
params.push(field)
}

let args = node.u.call.args
if num_expected_fields == 0 and args.size == 0 {
Expand All @@ -577,95 +582,186 @@ def TypeChecker::check_enum_constructor(&this, node: &AST): &Type {
return variant.parent.type
}

for let i = 0; i < args.size; i++ {
let arg = args[i]
if arg.label? {
.error(Error::new(arg.label_span, "Labels are not allowed for value enum constructors"))
.check_call_args(node, params)
params.clear()
return enom.type
}

def TypeChecker::check_call_args_labelled(&this, node: &AST, params: &Vector<&Variable>, start: u32) {
let args = node.u.call.args
let callee = node.u.call.callee
let new_args = Vector<&Argument>::new()

let kwargs = Map<str, &Argument>::new()
let expected_params = Map<str, &Variable>::new()

for let i = 0; i < params.size; i++ {
let param = params.at(i)
if not param.sym? {
.error(Error::new(param.type.span, "Not allowed to have unlabeled parameter here"))
continue
}
expected_params[param.sym.name] = param
if i < start {
let arg = args[i]
kwargs[param.sym.name] = arg
new_args.push(args[i])
}
}

if i < num_expected_fields {
let expected = variant.get_field(i)
let arg_type = .check_expression(arg.expr, hint: expected.type)
if not arg_type? then continue
if not arg_type.eq(expected.type) {
if {
expected.sym? => .error(Error::new_hint(
arg.expr.span, f"Expected shared field `{expected.sym.name}` with type {expected.type.str()}, but got {arg_type.str()}",
expected.sym.span, "Field defined here"
))
else => .error(Error::new_hint(
arg.expr.span, `{variant.sym.display} field {i} has type {expected.type.str()}, but got {arg_type.str()}`,
variant.sym.span, "Field defined here"
))
}
for let i = start; i < args.size; i++ {
let arg = args.at(i)
// TODO: At parsing time, try and get implicit labels for unlabeled arguments such as for identifiers
if not arg.label? {
.error(Error::new(arg.expr.span, "Can't have positional arguments after labelled arguments"))
continue
}
let param_item = expected_params.get_item(arg.label)
let kwarg_item = kwargs.get_item(arg.label)

let expected_type: &Type = null
if {
not param_item? => .error(Error::new(
arg.label_span, f"Unknown labelled argument `{arg.label}`"
))
kwarg_item? => .error(Error::new_hint(
arg.label_span, f"Duplicate argument for parameter `{arg.label}`",
kwarg_item.value.expr.span, "Previously specified here"
))
else => {
kwargs[arg.label] = arg
expected_type = param_item.value.type
}
} else {
.error(Error::new(arg.expr.span, `Unknown extra argument for {variant.sym.display}`))
}
let arg_type = .check_expression(arg.expr)
if arg_type? and expected_type? and not arg_type.eq(expected_type) {
.error(Error::new_hint(
arg.expr.span, f"Expected `{arg.label}` with type {expected_type.str()}, but got {arg_type.str()}",
param_item.value.sym.span, "Parameter defined here"
))
}
}

if args.size < num_expected_fields {
.error(Error::new(node.u.call.close_paren_span, `{variant.sym.display} needs {num_expected_fields} fields, got {args.size}`))
for let i = start; i < params.size; i++ {
let param = params.at(i)
assert param.sym?, f"Expected a symbol for parameter {i}"

let item = kwargs.get_item(param.sym.name)
if item? {
new_args.push(item.value)

} else if param.default_value? {
let new_arg = Argument::new(param.default_value)
new_args.push(new_arg)

} else {
// Currently... this can never be reached, since we only go to this function AFTER
// we have seen a default arg. In the future, we might want to allow all arguments to
// be unordered as long as the positional ones are provided somewhere, and in that case
// this check would be necessary.
.error(Error::new_hint(
node.u.call.close_paren_span, f"Missing required argument `{param.sym.name}` of type {param.type.str()}",
param.sym.span, "Parameter defined here"
))
}
}

return variant.parent.type
args.free()
kwargs.free()
expected_params.free()
node.u.call.args = new_args
}


def TypeChecker::check_call_args(&this, node: &AST, params: &Vector<&Variable>, is_variadic: bool = false) {
let args = node.u.call.args

for let i = 0; i < params.size; i += 1 {
for let i = 0; i < params.size; i++ {
let param = params.at(i)
// Normal case
if i < args.size {
let arg = args.at(i)

if arg.label? and not arg.label.eq(param.sym.name) {
.error(Error::new(arg.label_span, `Argument label '{arg.label}' does not match parameter name '{param.sym.name}'`))
}

let arg_type = .check_expression(arg.expr, hint: param.type)
if not arg_type? or not param.type? then continue
if not arg_type.eq(param.type) {
.error(Error::new(arg.expr.span, `Argument {param.sym.name} has type {arg_type.str()} but expected {param.type.str()}`))
// We would only get here if we have not gone through the labelled arguments yet
if i >= args.size {
if param.default_value? {
let new_arg = Argument::new(param.default_value)
args.push(new_arg)
} else {
if param.sym? {
.error(Error::new_hint(
node.u.call.close_paren_span, f"Missing required argument `{param.sym.name}` of type {param.type.str()}",
param.sym.span, "Parameter defined here"
))
} else {
.error(Error::new(node.u.call.close_paren_span, `Missing required field {i} of type {param.type.str()}`))
}
}
continue
}

// Default argument case
} else if param.default_value? {
let arg = args.at(i)
if arg.label? {
if {
not param.sym? => .error(Error::new(
arg.label_span, "Cannot use a labelled argument for a non-labeled parameter"
))
not arg.label.eq(param.sym.name) => if {
// If we have started looking at the default arguments, we can start allowing
// unordered arguments when they are labelled
param.default_value? => {
if is_variadic {
.error(Error::new(
arg.label_span, `Variadic functions not allowed with default arguments`,
))
}

// FIXME: We should not be evaluating the default argument here during every call,
// we should be evaluating it at the time of checking the function declaration and using
// a cached value here
let new_arg = Argument::new(param.default_value)
// Plus the default argument to the list of arguments
args.push(new_arg)
.check_call_args_labelled(node, params, i)
break
}
// If this parameter doesn't have a default value, then we should error
else => {
.error(Error::new_hint(
arg.label_span, f"Expected positional argument `{param.sym.name}`, but got label `{arg.label}`",
param.sym.span, "Parameter defined here"
))
}
// Fallthrough, and check the argument as if it were positional anyway
}
// Fallthrough
else => {}
}
}

} else {
.error(Error::new(node.u.call.close_paren_span, `Missing required argument {param.sym.name}`))
return
let arg_type = .check_expression(arg.expr, hint: param.type)
if not arg_type? or not param.type? {
continue
}
if not arg_type.eq(param.type) {
if param.sym? {
.error(Error::new_hint(
arg.expr.span, f"Expected `{param.sym.name}` with type {param.type.str()}, but got {arg_type.str()}",
param.sym.span, "Parameter defined here"
))
} else {
.error(Error::new(arg.expr.span, `Expected field {i} with type {param.type.str()}, but got {arg_type.str()}`))
}
}
}

if is_variadic {
if args.size < params.size {
assert .o.program.errors.size > 1, "Should have errored already"
return
}
// The arguments might have been updated by `check_call_args_labelled`, so we need to re-fetch them
args = node.u.call.args

for let i = params.size; i < args.size; i += 1 {
if is_variadic {
for let i = params.size; i < args.size; i++ {
let arg = args.at(i)
let arg_type = .check_expression(arg.expr)
if not arg_type? then continue
if not arg_type? {
continue
}
}

} else if params.size < args.size {
// Too many arguments... check them but throw errors
let err_msg = f"Unexpected argument, expected only {params.size}"
for let i = params.size; i < args.size; i += 1 {
} else {
for let i = params.size; i < args.size; i++ {
let arg = args.at(i)
.error(Error::new(arg.expr.span, err_msg))
let arg_type = .check_expression(arg.expr)
if not arg_type? then continue
.error(Error::new(arg.expr.span, `Unexpected argument, expected only {params.size}`))
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions meta/gen_bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ mkdir -p build
set -e

echo "[+] Testing 3-stage bootstrap for validity"
$initial compiler/main.oc -o build/stage1
./build/stage1 compiler/main.oc -o build/stage2
./build/stage2 compiler/main.oc -o build/stage3
$initial -d compiler/main.oc -o build/stage1
./build/stage1 -d compiler/main.oc -o build/stage2
./build/stage2 -d compiler/main.oc -o build/stage3
if diff build/stage2.c build/stage3.c; then
echo "[+] Verification successful!"
echo
Expand Down
46 changes: 30 additions & 16 deletions meta/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import shutil
from subprocess import run, PIPE
import argparse
import re
from ast import literal_eval
from dataclasses import dataclass
from enum import Enum
Expand Down Expand Up @@ -97,6 +98,33 @@ def get_expected(filename) -> Optional[Expected]:

return Expected(Result.SKIP_REPORT, None)

def check_lsp_output(smol, big):
if not type(smol) == type(big):
return False
if isinstance(smol, (int, float, str)):
return smol == big

if isinstance(smol, list):
for i, val in enumerate(smol):
if not any(check_lsp_output(val, big_val) for big_val in big):
return False
return True

if isinstance(smol, dict):
for key, value in smol.items():
if key not in big:
return False

# For file key, we want assume the smol value is a regex
if key == "file":
if not re.match(value, big[key]):
return False

if not check_lsp_output(value, big[key]):
return False
return True
return smol == big

def handle_lsp_test(compiler: str, num: int, path: Path, expected: Expected, debug: bool) -> Tuple[bool, str, Path]:
cmd = f'{compiler} lsp {expected.value.flags} {path}'
process = run(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
Expand All @@ -109,25 +137,11 @@ def handle_lsp_test(compiler: str, num: int, path: Path, expected: Expected, deb
except json.JSONDecodeError:
return False, f"Failed to parse JSON output: {output}", path

def normalize_paths(data):
if isinstance(data, dict):
for key, value in data.items():
if key == "file":
data[key] = Path(value).basename()
else:
normalize_paths(value)
elif isinstance(data, list):
for value in data:
normalize_paths(value)

wanted = expected.value.value
normalize_paths(output)
normalize_paths(wanted)

if output == wanted:
if check_lsp_output(wanted, output):
return True, "(Success)", path

return False, f"Expected LSP output does not match\n expected: {json.dumps(wanted)}\n got: {json.dumps(output)}", path
return False, f"Expected LSP output does not match\n expected: {wanted}\n got: {output}", path

def handle_test(compiler: str, num: int, path: Path, expected: Expected, debug: bool) -> Tuple[bool, str, Path]:
exec_name = f'./build/tests/{path.stem}-{num}'
Expand Down
2 changes: 1 addition & 1 deletion std/logging.oc
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def vlog(level: LogLevel, fmt: str, vargs: VarArgs) {
time::strftime(time_buf, 64, log_time_format, tm_info)
print(`({time_buf}) `)
}

match level {
Debug => print("[DEBUG] ")
Info => print("[INFO] ")
Expand Down
7 changes: 7 additions & 0 deletions tests/bad/func/bad_arg_type.oc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// fail: Expected `x` with type i32, but got bool

def foo(x: i32) { }

def main() {
foo(true)
}
File renamed without changes.
8 changes: 8 additions & 0 deletions tests/bad/func/duplicate_labelled.oc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// fail: Duplicate argument for parameter `b`


def foo(a: u32, b: u32 = 4, c: u32 = 5): u32 => a + b + c

def main() {
foo(1, 2, b: 7)
}
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions tests/bad/func/labelled_instead_of_positional.oc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// fail: Expected positional argument `a`, but got label `c`


def foo(a: u32, b: u32, c: u32 = 5): u32 => a + b + c

def main() {
foo(c: 7)
}
9 changes: 9 additions & 0 deletions tests/bad/func/missing_required_arg.oc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// fail: Missing required argument `b` of type i32

def foo(a: i32, b: i32): i32 {
return a + b
}

def main() {
foo(1)
}
File renamed without changes.
Loading
Loading