Browse Source

Fixed various bugs and added more tests. Also added nim.cfg

master
Mattia Giambirtone 4 weeks ago
parent
commit
3dead5a555
  1. 1
      nim.cfg
  2. 145
      src/frontend/compiler.nim
  3. 5
      src/frontend/parser.nim
  4. 18
      src/main.nim
  5. 12
      tests/closures2.pn
  6. 21
      tests/cross_shadowing.pn
  7. 2
      tests/dispatch.pn
  8. 1
      tests/generics.pn
  9. 2
      tests/mutable.pn

1
nim.cfg

@ -0,0 +1 @@
--hints:off --warnings:off

145
src/frontend/compiler.nim

@ -194,7 +194,7 @@ type
# List of closed-over variables
closures: seq[Name]
# Compiler procedures called by pragmas
compilerProcs: TableRef[string, proc (self: Compiler, pragma: Pragma, node: ASTNode, name: Name)]
compilerProcs: TableRef[string, proc (self: Compiler, pragma: Pragma, name: Name)]
# Stores line data for error reporting
lines: seq[tuple[start, stop: int]]
# The source of the current module,
@ -237,9 +237,9 @@ proc findByModule(self: Compiler, name: string): seq[Name]
proc findByType(self: Compiler, name: string, kind: Type, depth: int = -1): seq[Name]
proc compare(self: Compiler, a, b: Type): bool
proc patchReturnAddress(self: Compiler, pos: int)
proc handleMagicPragma(self: Compiler, pragma: Pragma, node: ASTnode, name: Name)
proc handlePurePragma(self: Compiler, pragma: Pragma, node: ASTnode, name: Name)
proc dispatchPragmas(self: Compiler, node: ASTnode, name: Name)
proc handleMagicPragma(self: Compiler, pragma: Pragma, name: Name)
proc handlePurePragma(self: Compiler, pragma: Pragma, name: Name)
proc dispatchPragmas(self: Compiler, name: Name)
proc funDecl(self: Compiler, node: FunDecl, name: Name)
proc typeDecl(self: Compiler, node: TypeDecl, name: Name)
proc compileModule(self: Compiler, moduleName: string)
@ -260,7 +260,7 @@ proc newCompiler*(replMode: bool = false): Compiler =
result.currentFunction = nil
result.replMode = replMode
result.currentModule = ""
result.compilerProcs = newTable[string, proc (self: Compiler, pragma: Pragma, node: ASTNode, name: Name)]()
result.compilerProcs = newTable[string, proc (self: Compiler, pragma: Pragma, name: Name)]()
result.compilerProcs["magic"] = handleMagicPragma
result.compilerProcs["pure"] = handlePurePragma
result.source = ""
@ -491,10 +491,12 @@ proc resolve(self: Compiler, name: string): Name =
## Traverses all existing namespaces and returns
## the first object with the given name. Returns
## nil when the name can't be found. Note that
## when a declaration is first resolved, it is
## also compiled on-the-fly
## when a type or function declaration is first
## resolved, it is also compiled on-the-fly
for obj in reversed(self.names):
if obj.ident.token.lexeme == name:
if obj.kind == NameKind.Argument and obj.belongsTo != self.currentFunction:
continue
if obj.owner != self.currentModule:
# We don't own this name, but we
# may still have access to it
@ -573,21 +575,28 @@ proc getStackPos(self: Compiler, name: Name): int =
# Argument of a function we haven't compiled yet (or one that we're
# not in). Ignore it, as it won't exist at runtime
continue
elif not variable.belongsTo.isNil() and variable.belongsTo.valueType.isBuiltinFunction:
# Builtin functions don't exist at runtime either, so variables belonging to them
# are not present in the stack
continue
elif not variable.valueType.isNil() and variable.valueType.kind == Generic:
# Generics are also a purely compile-time construct and are therefore
# ignored as far as stack positioning goes
continue
elif not variable.belongsTo.isNil():
if variable.belongsTo.valueType.isBuiltinFunction:
# Builtin functions don't exist at runtime either, so variables belonging to them
# are not present in the stack
continue
elif variable.valueType.kind == Generic:
# Generics are also a purely compile-time construct and are therefore
# ignored as far as stack positioning goes
continue
elif variable.belongsTo != name.belongsTo:
# Since referencing a function immediately compiles it, this means
# that if there's a function A with an argument x that calls another
# function B with an argument also named x, that second "x" would
# shadow the first one, leading to an incorrect stack offset
continue
elif variable.owner != self.currentModule:
# We don't own this variable, so we check
# if the owner exported it to us. If not,
# we skip it and pretend it doesn't exist
if variable.isPrivate or not variable.exported:
continue
elif name == variable:
if name == variable:
# After all of these checks, we can
# finally check whether the two names
# match (note: this also includes scope
@ -931,20 +940,35 @@ proc typeToStr(self: Compiler, typ: Type): string =
proc findByName(self: Compiler, name: string): seq[Name] =
## Looks for objects that have been already declared
## with the given name. Returns all objects that apply
## with the given name. Returns all objects that apply.
## As with resolve(), this will cause type and function
## declarations to be compiled on-the-fly
for obj in reversed(self.names):
if obj.ident.token.lexeme == name:
if obj.owner != self.currentModule:
if obj.isPrivate or not obj.exported:
continue
result.add(obj)
for n in result:
if n.resolved:
continue
n.resolved = true
case n.kind:
of NameKind.CustomType:
self.typeDecl(TypeDecl(n.node), n)
of NameKind.Function:
if not n.valueType.isGeneric:
self.funDecl(FunDecl(n.node), n)
else:
discard
proc findByModule(self: Compiler, name: string): seq[Name] =
## Looks for objects that have been already declared AS
## public within the given module. Returns all objects that apply
for obj in reversed(self.names):
if obj.owner == name:
if not obj.isPrivate and obj.owner == name:
result.add(obj)
@ -1134,8 +1158,6 @@ proc endScope(self: Compiler) =
return
for name in self.names:
if name.depth > self.depth:
if not name.belongsTo.isNil() and not name.belongsTo.resolved:
continue
names.add(name)
#[if not name.resolved:
# TODO: Emit a warning?
@ -1148,7 +1170,7 @@ proc endScope(self: Compiler) =
if name.kind == NameKind.Var:
inc(popCount)
elif name.kind == NameKind.Argument:
if not name.belongsTo.valueType.isBuiltinFunction and name.belongsTo.resolved:
if not name.belongsTo.valueType.isBuiltinFunction and name.belongsTo.resolved and not name.belongsTo.valueType.isGeneric:
# We don't pop arguments to builtin functions because those don't
# actually have scopes: their arguments are temporaries on the stack
inc(popCount)
@ -1202,7 +1224,6 @@ proc endScope(self: Compiler) =
inc(idx)
proc unpackGenerics(self: Compiler, condition: Expression, list: var seq[tuple[match: bool, kind: Type]], accept: bool = true) =
## Recursively unpacks a type constraint in a generic type
case condition.kind:
@ -1227,7 +1248,7 @@ proc unpackGenerics(self: Compiler, condition: Expression, list: var seq[tuple[m
self.error("invalid type constraint in generic declaration", condition)
proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
proc declareName(self: Compiler, node: ASTNode, mutable: bool = false) =
## Statically declares a name into the current scope.
## "Declaring" a name only means updating our internal
## list of identifiers so that further calls to resolve()
@ -1235,15 +1256,16 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
## declare a variable at runtime: the value is already
## on the stack
var declaredName: string = ""
var n: Name
case node.kind:
of NodeKind.varDecl:
var node = VarDecl(node)
# Creates a new Name entry so that self.identifier emits the proper stack offset
if self.names.high() > 16777215:
# If someone ever hits this limit in real-world scenarios, I swear I'll
# slap myself 100 times with a sign saying "I'm dumb". Mark my words
self.error("cannot declare more than 16777215 variables at a time")
declaredName = node.name.token.lexeme
# Creates a new Name entry so that self.identifier emits the proper stack offset
self.names.add(Name(depth: self.depth,
ident: node.name,
isPrivate: node.isPrivate,
@ -1256,12 +1278,13 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
kind: NameKind.Var,
node: node
))
n = self.names[^1]
if mutable:
self.names[^1].valueType.mutable = true
result = self.names[^1]
of NodeKind.funDecl:
var node = FunDecl(node)
result = Name(depth: self.depth,
declaredName = node.name.token.lexeme
var fn = Name(depth: self.depth,
isPrivate: node.isPrivate,
isConst: false,
owner: self.currentModule,
@ -1276,31 +1299,32 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
line: node.token.line,
kind: NameKind.Function,
belongsTo: self.currentFunction)
n = fn
# First we declare the function's generics, if it has any.
# This is because the function's return type may in itself
# be a generic, so it needs to exist first
var constraints: seq[tuple[match: bool, kind: Type]] = @[]
for gen in node.generics:
self.unpackGenerics(gen.cond, constraints)
self.names.add(Name(depth: result.depth + 1,
self.names.add(Name(depth: fn.depth + 1,
isPrivate: true,
valueType: Type(kind: Generic, name: gen.name.token.lexeme, mutable: false, cond: constraints),
codePos: 0,
isLet: false,
line: result.node.token.line,
belongsTo: result,
line: fn.node.token.line,
belongsTo: fn,
ident: gen.name,
owner: self.currentModule))
constraints = @[]
if not node.returnType.isNil():
result.valueType.returnType = self.inferOrError(node.returnType, allowGeneric=true)
self.names.add(result)
fn.valueType.returnType = self.inferOrError(node.returnType, allowGeneric=true)
self.names.add(fn)
# We now declare and typecheck the function's
# arguments
for argument in FunDecl(result.node).arguments:
for argument in FunDecl(fn.node).arguments:
if self.names.high() > 16777215:
self.error("cannot declare more than 16777215 variables at a time")
self.names.add(Name(depth: result.depth + 1,
self.names.add(Name(depth: fn.depth + 1,
isPrivate: true,
owner: self.currentModule,
isConst: false,
@ -1309,12 +1333,12 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
codePos: 0,
isLet: false,
line: argument.name.token.line,
belongsTo: result,
belongsTo: fn,
kind: NameKind.Argument
))
result.valueType.args.add((self.names[^1].ident.token.lexeme, self.names[^1].valueType))
fn.valueType.args.add((self.names[^1].ident.token.lexeme, self.names[^1].valueType))
if node.generics.len() > 0:
result.valueType.isGeneric = true
fn.valueType.isGeneric = true
of NodeKind.importStmt:
var node = ImportStmt(node)
var name = node.moduleName.token.lexeme.extractFilename().replace(".pn", "")
@ -1326,11 +1350,12 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
kind: NameKind.Module,
isPrivate: false
))
result = self.names[^1]
n = self.names[^1]
else:
discard # TODO: Types, enums
self.dispatchPragmas(n)
for name in self.findByName(declaredName):
if name == result:
if name == n:
continue
elif (name.kind == NameKind.Var and name.depth == self.depth) or name.kind in [NameKind.Module, NameKind.CustomType, NameKind.Enum]:
self.error(&"attempt to redeclare '{name.ident.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}")
@ -1357,47 +1382,49 @@ proc patchBreaks(self: Compiler) =
self.patchJump(brk)
proc handleMagicPragma(self: Compiler, pragma: Pragma, node: ASTNode, name: Name) =
proc handleMagicPragma(self: Compiler, pragma: Pragma, name: Name) =
## Handles the "magic" pragma. Assumes the given name is already
## declared
if pragma.args.len() != 1:
self.error("'magic' pragma: wrong number of arguments")
elif pragma.args[0].kind != strExpr:
self.error("'magic' pragma: wrong type of argument (constant string expected)")
elif node.kind != NodeKind.funDecl:
elif name.node.kind != NodeKind.funDecl:
self.error("'magic' pragma is not valid in this context")
var node = FunDecl(node)
var node = FunDecl(name.node)
name.valueType.isBuiltinFunction = true
name.valueType.builtinOp = pragma.args[0].token.lexeme[1..^2]
# The magic pragma ignores the function's body
node.body = nil
proc handlePurePragma(self: Compiler, pragma: Pragma, node: ASTNode, name: Name) =
proc handlePurePragma(self: Compiler, pragma: Pragma, name: Name) =
## Handles the "pure" pragma
case node.kind:
case name.node.kind:
of NodeKind.funDecl:
FunDecl(node).isPure = true
FunDecl(name.node).isPure = true
of lambdaExpr:
LambdaExpr(node).isPure = true
LambdaExpr(name.node).isPure = true
else:
self.error("'pure' pragma is not valid in this context")
proc dispatchPragmas(self: Compiler, node: ASTnode, name: Name) =
proc dispatchPragmas(self: Compiler, name: Name) =
## Dispatches pragmas bound to objects
if name.node.isNil():
return
var pragmas: seq[Pragma] = @[]
case node.kind:
case name.node.kind:
of NodeKind.funDecl, NodeKind.typeDecl, NodeKind.varDecl:
pragmas = Declaration(node).pragmas
pragmas = Declaration(name.node).pragmas
of lambdaExpr:
pragmas = LambdaExpr(node).pragmas
pragmas = LambdaExpr(name.node).pragmas
else:
discard # Unreachable
for pragma in pragmas:
if pragma.name.token.lexeme notin self.compilerProcs:
self.error(&"unknown pragma '{pragma.name.token.lexeme}'")
self.compilerProcs[pragma.name.token.lexeme](self, pragma, node, name)
self.compilerProcs[pragma.name.token.lexeme](self, pragma, name)
proc patchReturnAddress(self: Compiler, pos: int) =
@ -1629,7 +1656,7 @@ proc identifier(self: Compiler, node: IdentExpr) =
else:
# Static name resolution, loads value at index in the stack. Very fast. Much wow.
self.emitByte(LoadVar, node.token.line)
# No need to check for -1 here: we already did a nil check above!ù
# No need to check for -1 here: we already did a nil check above!
self.emitBytes(self.getStackPos(s).toTriple(), node.token.line)
@ -1764,6 +1791,7 @@ proc specialize(self: Compiler, name: Name, args: seq[Expression]): Name =
var mapping: TableRef[string, Type] = newTable[string, Type]()
var kind: Type
result = deepCopy(name)
result.valueType.isGeneric = false
case name.kind:
of NameKind.Function:
# This first loop checks if a user tries to reassign a generic's
@ -1793,6 +1821,7 @@ proc specialize(self: Compiler, name: Name, args: seq[Expression]): Name =
))
if result.valueType.returnType.kind == Generic:
result.valueType.returnType = mapping[result.valueType.returnType.name]
# self.funDecl(FunDecl(result.node), result)
else:
discard # TODO: Custom user-defined types
@ -1825,6 +1854,7 @@ proc callExpr(self: Compiler, node: CallExpr): Name {.discardable.} =
# very last moment to compile it, once
# that info is available to us
result = self.specialize(result, argExpr)
self.funDecl(FunDecl(result.node), result)
# Now we call it
self.generateCall(result, argExpr, node.token.line)
of NodeKind.callExpr:
@ -1990,7 +2020,7 @@ proc importStmt(self: Compiler, node: ImportStmt) =
let filename = splitPath(node.moduleName.token.lexeme).tail
try:
self.compileModule(node.moduleName.token.lexeme)
discard self.declareName(node)
self.declareName(node)
except IOError:
self.error(&"""could not import '{filename}': {getCurrentExceptionMsg()}""")
except OSError:
@ -2148,6 +2178,8 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
## Compiles function declarations
if node.token.kind == Operator and node.name.token.lexeme in [".", ]:
self.error(&"Due to current compiler limitations, the '{node.name.token.lexeme}' operator cannot be overridden", node.name)
if name.valueType.isBuiltinFunction:
return
var node = node
var jmp: int
# We store the current function
@ -2177,6 +2209,7 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
else:
self.chunk.cfi.add(0.toDouble())
if BlockStmt(node.body).code.len() == 0:
raise newException(IndexDefect, "")
self.error("cannot declare function with empty body")
# Since the deferred array is a linear
# sequence of instructions and we want
@ -2241,9 +2274,8 @@ proc declaration(self: Compiler, node: Declaration) =
## the first time
case node.kind:
of NodeKind.varDecl, NodeKind.funDecl, NodeKind.typeDecl:
self.dispatchPragmas(node, self.declareName(node))
self.declareName(node)
if node.kind == NodeKind.varDecl:
self.names[^1].resolved = true
# We compile this immediately because we
# need to keep the stack in the right state
# at runtime
@ -2285,7 +2317,10 @@ proc compileModule(self: Compiler, moduleName: string) =
## using the compiler's internal parser and lexer objects
var path = ""
for i, searchPath in moduleLookupPaths:
path = joinPath(getCurrentDir(), joinPath(searchPath, moduleName))
if searchPath == "":
path = joinPath(getCurrentDir(), joinPath(splitPath(self.file).head, moduleName))
else:
path = joinPath(getCurrentDir(), joinPath(searchPath, moduleName))
if fileExists(path):
break
elif i == searchPath.high():

5
src/frontend/parser.nim

@ -730,7 +730,10 @@ proc importStmt(self: Parser, fromStmt: bool = false): Statement =
lexer.fillSymbolTable()
var path = ""
for i, searchPath in moduleLookupPaths:
path = joinPath(getCurrentDir(), joinPath(searchPath, moduleName))
if searchPath == "":
path = joinPath(getCurrentDir(), joinPath(splitPath(self.file).head, moduleName))
else:
path = joinPath(getCurrentDir(), joinPath(searchPath, moduleName))
if fileExists(path):
break
elif i == searchPath.high():

18
src/main.nim

@ -67,7 +67,7 @@ proc repl =
tokenizer.fillSymbolTable()
editor.bindEvent(jeQuit):
stdout.styledWriteLine(fgGreen, "Goodbye!")
editor.prompt = ""
editor.prompt = "=> "
keep = false
input = ""
editor.bindKey("ctrl+a"):
@ -76,10 +76,6 @@ proc repl =
editor.content.`end`()
while keep:
try:
# We incrementally add content to the input
# so that you can, for example, define a function
# then press enter and use it at the next iteration
# of the read loop
input = editor.read()
if input.len() == 0:
continue
@ -153,7 +149,7 @@ proc repl =
fgYellow, &"'{exc.file.extractFilename()}'", fgRed, ", line ", fgYellow, $exc.line, fgRed, " at ", fgYellow, &"'{exc.lexeme.escape()}'",
fgRed, ": ", fgGreen , getCurrentExceptionMsg())
styledEcho fgBlue, "Source line: " , fgDefault, line
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(exc.lexeme)) & "^".repeat(relPos.stop - relPos.start - line.find(exc.lexeme))
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(exc.lexeme)) & "^".repeat(abs(relPos.stop - relPos.start - line.find(exc.lexeme)))
except ParseError:
input = ""
let exc = ParseError(getCurrentException())
@ -169,7 +165,7 @@ proc repl =
fgYellow, &"'{exc.module}'", fgRed, ", line ", fgYellow, $lineNo, fgRed, " at ", fgYellow, &"'{lexeme}'",
fgRed, ": ", fgGreen , getCurrentExceptionMsg())
styledEcho fgBlue, "Source line: " , fgDefault, line
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(relPos.stop - relPos.start - line.find(lexeme))
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(abs(relPos.stop - relPos.start - line.find(lexeme)))
except CompileError:
let exc = CompileError(getCurrentException())
let lexeme = exc.node.token.lexeme
@ -184,7 +180,7 @@ proc repl =
fgYellow, &"'{exc.module}'", fgRed, ", line ", fgYellow, $lineNo, fgRed, " at ", fgYellow, &"'{lexeme}'",
fgRed, ": ", fgGreen , getCurrentExceptionMsg())
styledEcho fgBlue, "Source line: " , fgDefault, line
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(relPos.stop - relPos.start - line.find(lexeme))
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(abs(relPos.stop - relPos.start - line.find(lexeme)))
except SerializationError:
let exc = SerializationError(getCurrentException())
stderr.styledWriteLine(fgRed, "A fatal error occurred while (de-)serializing", fgYellow, &"'{exc.file}'", fgGreen, ": ", getCurrentExceptionMsg())
@ -280,7 +276,7 @@ proc runFile(f: string, interactive: bool = false, fromString: bool = false, dum
fgYellow, &"'{exc.file.extractFilename()}'", fgRed, ", line ", fgYellow, $exc.line, fgRed, " at ", fgYellow, &"'{exc.lexeme.escape()}'",
fgRed, ": ", fgGreen , getCurrentExceptionMsg())
styledEcho fgBlue, "Source line: " , fgDefault, line
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(exc.lexeme)) & "^".repeat(relPos.stop - relPos.start - line.find(exc.lexeme))
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(exc.lexeme)) & "^".repeat(abs(relPos.stop - relPos.start - line.find(exc.lexeme)))
except ParseError:
let exc = ParseError(getCurrentException())
let lexeme = exc.token.lexeme
@ -295,7 +291,7 @@ proc runFile(f: string, interactive: bool = false, fromString: bool = false, dum
fgYellow, &"'{exc.module}'", fgRed, ", line ", fgYellow, $lineNo, fgRed, " at ", fgYellow, &"'{lexeme}'",
fgRed, ": ", fgGreen , getCurrentExceptionMsg())
styledEcho fgBlue, "Source line: " , fgDefault, line
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(relPos.stop - relPos.start - line.find(lexeme))
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(abs(relPos.stop - relPos.start - line.find(lexeme)))
except CompileError:
let exc = CompileError(getCurrentException())
let lexeme = exc.node.token.lexeme
@ -310,7 +306,7 @@ proc runFile(f: string, interactive: bool = false, fromString: bool = false, dum
fgYellow, &"'{exc.module}'", fgRed, ", line ", fgYellow, $lineNo, fgRed, " at ", fgYellow, &"'{lexeme}'",
fgRed, ": ", fgGreen , getCurrentExceptionMsg())
styledEcho fgBlue, "Source line: " , fgDefault, line
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(relPos.stop - relPos.start - line.find(lexeme))
styledEcho fgCyan, " ".repeat(len("Source line: ") + line.find(lexeme)) & "^".repeat(abs(relPos.stop - relPos.start - line.find(lexeme)))
except SerializationError:
let exc = SerializationError(getCurrentException())
stderr.styledWriteLine(fgRed, "A fatal error occurred while (de-)serializing", fgYellow, &"'{exc.file}'", fgGreen, ": ", getCurrentExceptionMsg())

12
tests/closures2.pn

@ -0,0 +1,12 @@
import std;
fn makeAdder(x: int): fn (n: int): int {
fn adder(n: int): int {
return x + n;
}
return adder;
}
print(makeAdder(5)(2)); # 7

21
tests/cross_shadowing.pn

@ -0,0 +1,21 @@
# Tests shadowing of arguments and local variables
# across functions
import std;
fn first(x: int): int {
var y = x;
y = y + 1;
return y;
}
fn second(x: int): int {
var y = first(x);
y = y + 1;
return y;
}
print(second(0)); # 2

2
tests/dispatch.pn

@ -7,4 +7,4 @@ operator `+`(a: int): int {
+1; # Works: defined for int64
+1'i32; # Will not work
# +1'i32; # Will not work

1
tests/generics.pn

@ -1,3 +1,4 @@
# Tests generic functions
import std;

2
tests/mutable.pn

@ -1,4 +1,4 @@
# Tests var parameters
# Tests var parameters. TODO: They don't actually exist yet, they're just checked statically
import std;

Loading…
Cancel
Save