commit 9e76fca9771db1590fda9fa56124bd8a21f7b8d1 Author: Maxwell Jeffress Date: Sat Oct 25 21:28:16 2025 +1100 Initial commit diff --git a/.vim/syntax/funk.vim b/.vim/syntax/funk.vim new file mode 100644 index 0000000..865f47a --- /dev/null +++ b/.vim/syntax/funk.vim @@ -0,0 +1,68 @@ +" Vim syntax file +" Language: Funk +" Maintainer: Maxwell Jeffress +" Latest Revision: 25 October 2025 + +if exists("b:current_syntax") + finish +endif + +" Keywords +syn keyword funkKeyword func import +syn keyword funkConditional if while +syn keyword funkBoolean true false + +" Built-in functions +syn keyword funkBuiltin print println + +" Operators +syn match funkOperator "\v\=" +syn match funkOperator "\v\=\=" +syn match funkOperator "\v!\=" +syn match funkOperator "\v\>" +syn match funkOperator "\v\<" +syn match funkOperator "\v\>\=" +syn match funkOperator "\v\<\=" +syn match funkOperator "\v\+" +syn match funkOperator "\v-" +syn match funkOperator "\v\*" +syn match funkOperator "\v/" +syn match funkOperator "\v\^" +syn match funkOperator "\v\%" +syn match funkOperator "\v\&\&" +syn match funkOperator "\v\|\|" +syn match funkOperator "\v!" + +" Numbers +syn match funkNumber "\v<\d+>" +syn match funkNumber "\v<\d+\.\d+>" + +" Strings +syn region funkString start='"' end='"' skip='\\"' + +" Comments +syn match funkComment "\v//.*$" + +" Identifiers (function/variable names) +syn match funkIdentifier "\v<[a-zA-Z_][a-zA-Z0-9_]*>" + +" Special variables (arg0, arg1, etc.) +syn match funkSpecialVar "\varg\d+" + +" Delimiters +syn match funkDelimiter "\v[\(\)\{\}]" + +" Highlighting +hi def link funkKeyword Keyword +hi def link funkConditional Conditional +hi def link funkBoolean Boolean +hi def link funkBuiltin Function +hi def link funkOperator Operator +hi def link funkNumber Number +hi def link funkString String +hi def link funkComment Comment +hi def link funkIdentifier Identifier +hi def link funkSpecialVar Special +hi def link funkDelimiter Delimiter + +let b:current_syntax = "funk" diff --git a/src/lexer/lexer.cpp b/src/lexer/lexer.cpp new file mode 100644 index 0000000..ab6fa98 --- /dev/null +++ b/src/lexer/lexer.cpp @@ -0,0 +1,70 @@ +#include "lexer.h" +#include +#include +#include +#include +#include +#include +#include + +std::optional Lexer::consume() { + incrementor ++; + if (incrementor < file.size()) { + return file[incrementor]; + } else { + return {}; + } +} + +std::optional Lexer::peek(int ahead) { + if (incrementor + ahead < file.size()) { + return file[incrementor + ahead]; + } else { + return {}; + } +} + +bool Lexer::isDelimiter(char c) { + if (std::find(delimiters.begin(), delimiters.end(), c) != delimiters.end()) { + return true; + } else { + return false; + } +} + +/** + * @brief Constructs a Lexer object and tokenizes the provided input string. + * + * This constructor initializes the Lexer with the given input string and processes it + * to generate a list of tokens. It supports handling strings encapsulated by double quotes + * and uses specified delimiters to separate tokens. + * + * @param in The input string to be tokenized. + * @return A constructed Lexer instance with tokenized content stored in the `content` member. + */ +Lexer::Lexer(std::string in) : file(std::move(in)) { + std::string buf; + bool instring = false; + while (true) { + std::optional c = consume(); + if (c.has_value()) { + if (c.value() == '"') { + instring = !instring; + if (!instring) { + content.push_back(buf + '"'); + buf.clear(); + continue; + } + } + if (!instring && isDelimiter(c.value())) { + if (!buf.empty()) content.push_back(buf); + if (c.value() != ' ') content.emplace_back(1, c.value()); + buf.clear(); + } else { + buf += c.value(); + } + } else { + break; + } + } +} diff --git a/src/lexer/lexer.h b/src/lexer/lexer.h new file mode 100644 index 0000000..510d4df --- /dev/null +++ b/src/lexer/lexer.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +/** + * @class Lexer + * @brief The Lexer class processes input strings to tokenize and parse contents. + * + * This class is designed to take a given string input, tokenize it based on + * specific delimiters, and store the resulting tokens. It facilitates basic + * operations like consuming, peeking at characters, and identifying delimiters. + */ +class Lexer { + private: + std::vector delimiters = { + '(', ')', '{', '}', '.', '\n', ' ' + }; + std::string file; + size_t incrementor = -1; + std::optional consume(); + std::optional peek(int ahead = 1); + bool isDelimiter(char c); + + public: + std::vector content; + explicit Lexer(std::string in); +}; + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9bd9375 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "lexer/lexer.h" +#include "parser/parser.h" +#include "runner/runner.h" + +int main(int argc, char** argv) { + if (argc <= 1) { + std::cout << "Usage: " << argv[0] << " (file)" << std::endl; + exit(1); + } + std::string fileContent; + { + std::ifstream file(argv[1]); + if (file) { + std::string buf; + while (std::getline(file, buf)) { + fileContent += buf + "\n"; + } + } else { + std::cout << "Could not open file" << std::endl; + exit(1); + } + } + Lexer lexer(fileContent); + ASTCodeBlock codeBlock(lexer.content); + Executor executor(codeBlock, true); +} diff --git a/src/parser/parser.cpp b/src/parser/parser.cpp new file mode 100644 index 0000000..8616c4d --- /dev/null +++ b/src/parser/parser.cpp @@ -0,0 +1,292 @@ +#include "parser.h" + +#include +#include +#include +#include +#include +#include +#include + +ASTValue::ASTValue() : type(ValueType::None) {} +ASTValue::ASTValue(std::string in) : type(ValueType::String), value(in) {} +ASTValue::ASTValue(bool in) : type(ValueType::Bool), value(in) {} +ASTValue::ASTValue(long long in) : type(ValueType::Int), value(in) {} +ASTValue::ASTValue(double in) : type(ValueType::Float), value(in) {} + +ValueType ASTValue::getValueType(std::string in) { + if (in.size() < 1) { + return ValueType::None; + } + if (in.front() == '"' && in.back() == '"') { + return ValueType::String; + } + if (in == "true" || in == "false") { + return ValueType::Bool; + } + bool isInt = true; + bool isFloat = false; + for (const char &c : in) { + if (std::isdigit(c) == false) { + if (c == '.' && isFloat == false) { + isFloat = true; + isInt = false; + } else { + isInt = false; + isFloat = false; + break; + } + } + } + if (isInt) { + return ValueType::Int; + } + if (isFloat) { + if (in == ".") return ValueType::None; + return ValueType::Float; + } + return ValueType::None; + +} + +ASTFunction::ASTFunction(ASTCodeBlock body) : body(std::move(body)) {} +ASTFunction::ASTFunction() {} +ASTFunctionCall::ASTFunctionCall(std::string func, std::vector args) : func(std::move(func)), args(std::move(args)) {} +ASTIdentifier::ASTIdentifier(std::string in) : name(std::move(in)) {} + + +std::optional ASTValue::getString() { + if (type == ValueType::String && std::holds_alternative(value)) { + return std::get(value); + } else { + return {}; + } +} + +std::optional ASTValue::getInt() { + if (type == ValueType::Int && std::holds_alternative(value)) { + return std::get(value); + } else { + return {}; + } +} + +std::optional ASTValue::getFloat() { + if (type == ValueType::Float && std::holds_alternative(value)) { + return std::get(value); + } else { + return {}; + } +} + +std::optional ASTValue::getBool() { + if (type == ValueType::Bool && std::holds_alternative(value)) { + return std::get(value); + } else { + return {}; + } +} + +std::optional ASTCodeBlock::consume() { + if (iterator < content.size()) { + return content[iterator++]; // Post-increment: returns current, then increments + } + return {}; +} + +std::optional ASTCodeBlock::peek(int ahead) { + if (iterator + ahead < content.size()) { + return content[iterator + ahead]; + } + return {}; +} + +TokenType ASTCodeBlock::getTokenType() { + if (peek(0).has_value() == false) { + return TokenType::None; + } + + std::optional tokenv = peek(0); + std::string token; + if (tokenv.has_value()) { + token = tokenv.value(); + } else { + return TokenType::None; + } + + // Check for values first + if (ASTValue().getValueType(token) != ValueType::None) { + return TokenType::Value; + } + + // Check for special characters/keywords + if (token == "{") { + return TokenType::CodeBlockStart; + } + if (token == "}") { + return TokenType::CodeBlockEnd; + } + if (token == "(") { + return TokenType::OpenParen; + } + if (token == ")") { + return TokenType::CloseParen; + } + if (token == "\n") { + return TokenType::NewLine; + } + + // Check if current token is "func" followed by "{" + if (token == "func" && peek(1).has_value() && peek(1).value() == "{") { + return TokenType::Function; + } + + // Check if next token is "(" (function call) + if (peek(1).has_value() && peek(1).value() == "(") { + return TokenType::FunctionCallStart; + } + + // If none of the above, it's an identifier + return TokenType::Identifier; +} + +/** + * @brief Parses a block of code and creates an Abstract Syntax Tree (AST) representation. + * + * This method iterates through the tokens of a given block of code, evaluates the type + * of each token, and adds the corresponding node to the AST. + * + * Supported token types include: + * - Values (e.g., strings, booleans, integers, floats) + * - Identifiers + * - Functions with nested code blocks + * - Function calls with arguments + * - Nested code blocks + * + * The method utilizes a `switch` statement based on the `TokenType` to determine the + * appropriate handling of each token. Each token is either consumed, parsed, and converted + * to an appropriate AST node type or processed for special structures (e.g., functions, + * code blocks, or function calls). + * + * @remarks This method assumes valid tokenized input. If an end-of-file condition + * occurs while parsing functions or function calls, the program exits with an error. + */ +void ASTCodeBlock::parseBlock() { + while (true) { + std::optional token = peek(0); + if (token.has_value() == false) { + return; + } + TokenType tokenType = getTokenType(); + ValueType valueType = ASTValue().getValueType(token.value()); + std::optional currentToken = consume(); + + if (currentToken.has_value()) { + switch (tokenType) { + case TokenType::Value: { + switch (valueType) { + case ValueType::String: + nodes.emplace_back(std::make_shared(currentToken.value().substr(1, currentToken.value().size() - 2))); + break; + case ValueType::Bool: + nodes.emplace_back(std::make_shared(currentToken.value() == "true")); + break; + case ValueType::Int: + nodes.emplace_back(std::make_shared(std::stoll(currentToken.value()))); + break; + case ValueType::Float: + nodes.emplace_back(std::make_shared(std::stod(currentToken.value()))); + break; + default: + break; + } + break; + } + case TokenType::Identifier: + nodes.emplace_back(std::make_shared(currentToken.value())); + break; + case TokenType::Function: { + std::vector body; + consume(); + int depth = 1; + while (depth > 0) { + std::optional token = consume(); + if (token.has_value()) { + if (token.value() == "{") { + depth++; + } else if (token.value() == "}") { + depth--; + if (depth == 0) { + break; + } + } + body.push_back(token.value()); + } else { + std::cout << "Reached end of file while parsing function" << std::endl; + exit(1); + } + } + consume(); + nodes.emplace_back(std::make_shared(ASTCodeBlock(body))); + break; + } + case TokenType::FunctionCallStart: { + std::vector args; + std::optional fnName = peek(-1); + std::string fnNameStr; + if (fnName.has_value()) { + fnNameStr = fnName.value(); + } else { + std::cout << "Reached end of file while parsing function call" << std::endl; + exit(1); + } + while (getTokenType() != TokenType::CloseParen) { + std::optional token = consume(); + if (token.has_value()) { + args.push_back(token.value()); + } else { + std::cout << "Reached end of file while parsing function call" << std::endl; + exit(1); + } + } + consume(); + nodes.emplace_back(std::make_shared(fnNameStr, ASTCodeBlock(args).nodes)); + break; + } + case TokenType::CodeBlockStart: { + std::vector body; + int depth = 1; + while (depth > 0) { + std::optional token = consume(); + if (token.has_value()) { + if (token.value() == "{") { + depth++; + } else if (token.value() == "}") { + depth--; + if (depth == 0) { + break; + } + } + body.push_back(token.value()); + } else { + std::cout << "Reached end of file while parsing code block" << std::endl; + exit(1); + } + } + nodes.emplace_back(std::make_shared(body)); + break; + } + default: + break; + } + } else { + break; + } + } +} + +ASTCodeBlock::ASTCodeBlock() {} + +ASTCodeBlock::ASTCodeBlock(std::vector in) : content(std::move(in)) { + parseBlock(); +} \ No newline at end of file diff --git a/src/parser/parser.h b/src/parser/parser.h new file mode 100644 index 0000000..c4801b5 --- /dev/null +++ b/src/parser/parser.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ASTValue; +class ASTFunction; +class ASTFunctionCall; +class ASTCodeBlock; +class ASTIdentifier; + +typedef std::variant, std::shared_ptr, std::shared_ptr, std::shared_ptr, std::shared_ptr> ASTNode; +typedef std::variant RealValue; + +enum class ValueType { + Int, Float, String, Bool, None +}; + +enum class TokenType { + Identifier, Value, Function, FunctionCallStart, OpenParen, CloseParen, CodeBlockStart, CodeBlockEnd, NewLine, None +}; + +/** + * @class ASTValue + * @brief Represents a value in the Abstract Syntax Tree (AST). + * + * The ASTValue class encapsulates different types of values, including integers, + * floating-point numbers, strings, and boolean. It provides methods for type identification + * and value retrieval. + */ +class ASTValue { +private: + RealValue value; +public: + ValueType type; + ValueType getValueType(std::string in); + std::optional getString(); + std::optional getInt(); + std::optional getFloat(); + std::optional getBool(); + explicit ASTValue(std::string in); + explicit ASTValue(long long in); + explicit ASTValue(double in); + explicit ASTValue(bool in); + ASTValue(); +}; + +/** + * @class ASTCodeBlock + * @brief Represents a block of code in the Abstract Syntax Tree (AST). + * + * The ASTCodeBlock class is responsible for encapsulating and parsing a block + * of code represented as a sequence of strings. It maintains the raw content + * of the block, processes its elements, and classifies tokens to construct a + * structured representation. The parsed elements are stored as AST nodes. + * + * Functions provided include utilities for token parsing, peeking into + * upcoming tokens, identifying token types, and managing the iterator for + * sequential token processing. + */ +class ASTCodeBlock { + private: + std::vector content; + size_t iterator = 0; + void parseBlock(); + std::optional consume(); + std::optional peek(int ahead = 1); + TokenType getTokenType(); + public: + std::vector nodes; + explicit ASTCodeBlock(std::vector); + ASTCodeBlock(); +}; + +class ASTFunction { + public: + ASTCodeBlock body; + explicit ASTFunction(ASTCodeBlock body); + ASTFunction(); +}; + +class ASTFunctionCall { + public: + std::string func; + std::vector args; + ASTFunctionCall(std::string func, std::vector args); +}; + +class ASTIdentifier { + public: + std::string name; + explicit ASTIdentifier(std::string in); +}; + +ASTNode parser(std::vector in); diff --git a/src/runner/runner.cpp b/src/runner/runner.cpp new file mode 100644 index 0000000..d98e2c8 --- /dev/null +++ b/src/runner/runner.cpp @@ -0,0 +1,249 @@ +#include "runner.h" + +#include +#include +#include + +#include "../parser/parser.h" + +std::optional Executor::consume() { + if (iterator < code.nodes.size()) { + return code.nodes[iterator++]; + } + return {}; +} + +std::optional Executor::peek(int ahead) { + if (iterator + ahead < code.nodes.size()) { + return code.nodes[iterator + ahead]; + } + return {}; +} + +/** + * Constructs an `Executor` object. + * + * This constructor initializes the `Executor` by taking an abstract syntax tree (AST) code block as input, + * setting up the execution context, including variables, functions, and arguments, and continuously parsing + * and executing the AST nodes until the end of the block is reached. + * + * @param in The abstract syntax tree (AST) code block to execute. + * @param isInitCall A boolean flag to determine if this is the initial entry point to execution. + * If true, it runs the "main" function after setting up the context. + * @param scopeVals A map of variable names (strings) to their corresponding `ASTValue` objects used as + * the current scope of variables. + * @param scopeFns A map of function names (strings) to their corresponding `ASTFunction` objects used as + * the current set of functions within scope. + * @param args A vector of `ASTValue` objects passed as arguments for the context of this execution. + * + * @details + * - If arguments are provided, they are assigned to variables named `arg0`, `arg1`, etc., in the local scope. + * - Functions can be defined dynamically within the block and will be stored in the `functions` map. + * - Variable assignments and supported operators (e.g., '=', '==', '!=', etc.) are processed if encountered. + * - If a function call is encountered, the corresponding function body is executed in a new `Executor` context, + * passing along argument values and maintaining the state of variables and functions. + * - Special support for `import()` calls is provided when encountered in the root function. + * - If `isInitCall` is true, ensures the "main" function is executed after parsing and executing the AST. + * - The constructor uses recursive execution for nested function calls. + * + * @note Exits the process if critical execution errors occur (e.g., unexpected nodes or missing values). + */ +Executor::Executor(ASTCodeBlock in, bool isInitCall, std::map scopeVals, std::map scopeFns, std::vector args) : code(std::move(in)), variables(std::move(scopeVals)), functions(std::move(scopeFns)) { + for (size_t i = 0; i < args.size(); i++) { + variables["arg" + std::to_string(i)] = args[i]; + } + while (true) { + std::optional node = consume(); + if (node.has_value()) { + // for if we see an identifier + if (std::holds_alternative>(node.value())) { + std::optional next = consume(); + if (next.has_value()) { + // function assignment + // eg: main func { ... } + // eg: dingus func { ... } + if (std::holds_alternative>(next.value())) { + functions[std::get>(node.value())->name] = *std::get>(next.value()); + } else if (std::holds_alternative>(next.value())) { + std::string id = std::get>(next.value())->name; + if (id == "=") { + // setting a variable + std::optional valueNode = consume(); + if (valueNode.has_value()) { + ASTValue next; + if (std::holds_alternative>(valueNode.value())) { + variables[std::get>(node.value())->name] = *std::get>(valueNode.value()); + } else if (std::holds_alternative>(valueNode.value())) { + functions[std::get>(node.value())->name] = *std::get>(valueNode.value()); + } else { + std::cout << "Expected value or function after = sign" << std::endl; + exit(1); + } + + } else { + std::cout << "Expected value after = sign" << std::endl; + exit(1); + } + } else if (id == "==") { + + } else if (id == "!=") { + + } else if (id == ">") { + + } else if (id == ">=") { + + } else if (id == "<") { + + } else if (id == "<=") { + + } + } else { + std::cout << "Expected function or operator after identifier" << std::endl; + exit(1); + } + } + } + // if we see a function call + // note: we only accept calls to import() in the root function + if (std::holds_alternative>(node.value())) { + std::string fnName = std::get>(node.value())->func; + std::vector callArgNodes = std::get>(node.value())->args; + std::vector callArgs; + for (auto &callArgNode : callArgNodes) { + if (std::holds_alternative>(callArgNode)) { + callArgs.push_back(*std::get>(callArgNode)); + } + } + if (fnName == "import") { + // work on importing modules later + continue; + } + if (isInitCall) { + std::cout << "Function " << fnName << " not allowed in root function" << std::endl; + exit(1); + } + if (fnName == "print") { + for (ASTValue &arg : callArgs) { + if (arg.type == ValueType::String) { + std::optional argString = arg.getString(); + if (argString.has_value()) { + std::cout << argString.value(); + } else { + std::cout << "Type mismatch - expecting string but got something else" << std::endl; + } + } else if (arg.type == ValueType::Int) { + std::optional argInt = arg.getInt(); + if (argInt.has_value()) { + std::cout << argInt.value(); + } else { + std::cout << "Type mismatch - expecting int but got something else" << std::endl; + } + } else if (arg.type == ValueType::Float) { + std::optional argFloat = arg.getFloat(); + if (argFloat.has_value()) { + std::cout << argFloat.value(); + } else { + std::cout << "Type mismatch - expecting float but got something else" << std::endl; + } + } else if (arg.type == ValueType::Bool) { + std::optional argBool = arg.getBool(); + if (argBool.has_value()) { + std::cout << argBool.value(); + } else { + std::cout << "Type mismatch - expecting bool but got something else" << std::endl; + } + } else { + std::cout << "Type mismatch - expecting string, int, float, or bool but got something else" << std::endl; + } + } + } else if (fnName == "println") { + for (ASTValue &arg : callArgs) { + if (arg.type == ValueType::String) { + std::optional argString = arg.getString(); + if (argString.has_value()) { + std::cout << argString.value() << std::endl; + } else { + std::cout << "Type mismatch - expecting string but got something else" << std::endl; + } + } else if (arg.type == ValueType::Int) { + std::optional argInt = arg.getInt(); + if (argInt.has_value()) { + std::cout << argInt.value() << std::endl; + } else { + std::cout << "Type mismatch - expecting int but got something else" << std::endl; + } + } else if (arg.type == ValueType::Float) { + std::optional argFloat = arg.getFloat(); + if (argFloat.has_value()) { + std::cout << argFloat.value() << std::endl; + } else { + std::cout << "Type mismatch - expecting float but got something else" << std::endl; + } + } else if (arg.type == ValueType::Bool) { + std::optional argBool = arg.getBool(); + if (argBool.has_value()) { + std::cout << argBool.value() << std::endl; + } else { + std::cout << "Type mismatch - expecting bool but got something else" << std::endl; + } + } else { + std::cout << "Type mismatch - expecting string, int, float, or bool but got something else" << std::endl; + } + } + } else if (fnName == "if") { + if (callArgs.empty()) { + std::cout << "Expected at least one argument to if statement" << std::endl; + exit(1); + } + if (callArgs[0].type != ValueType::Bool) { + std::cout << "Expected first argument to if statement to be a boolean" << std::endl; + exit(1); + } + std::optional block = consume(); + if (!block.has_value()) { + std::cout << "If statement expects a body" << std::endl; + exit(1); + } + if (callArgs[0].getBool().value()) { + if (std::holds_alternative>(block.value())) { + Executor(*std::get>(block.value()), false, variables, functions); + } + } + } else if (fnName == "while") { + if (callArgs.empty()) { + std::cout << "Expected at least one argument to if statement" << std::endl; + exit(1); + } + if (callArgs[0].type != ValueType::Bool) { + std::cout << "Expected first argument to if statement to be a boolean" << std::endl; + exit(1); + } + std::optional block = consume(); + if (!block.has_value()) { + std::cout << "If statement expects a body" << std::endl; + exit(1); + } + if (callArgs[0].getBool().value()) { + while (callArgs[0].getBool().value()) { + if (std::holds_alternative>(block.value())) { + Executor(*std::get>(block.value()), false, variables, functions); + } + } + } + } else { + if (functions.find(fnName) != functions.end()) { + Executor(functions[fnName].body, false, variables, functions, callArgs); + } else { + std::cout << "Function " << fnName << " not found" << std::endl; + exit(1); + } + } + } + } else { + break; + } + } + if (isInitCall) { + Executor(functions["main"].body, false, variables, functions); + } +} \ No newline at end of file diff --git a/src/runner/runner.h b/src/runner/runner.h new file mode 100644 index 0000000..2323bb9 --- /dev/null +++ b/src/runner/runner.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "../parser/parser.h" + +/** + * @class Executor + * @brief Responsible for executing a sequence of operations defined in an abstract syntax tree (AST). + * + * This class provides execution functionality for ASTCodeBlock objects. + * It maintains a mapping of variables and functions that can be used within the + * context of execution. The class implements mechanisms for traversing AST nodes + * and consuming or peeking at individual nodes. + */ +class Executor { + private: + std::map functions; + std::map variables; + ASTCodeBlock code; + size_t iterator = 0; + std::optional consume(); + std::optional peek(int ahead = 1); + public: + explicit Executor(ASTCodeBlock in, bool isInitCall = false, std::map scopeVals = {}, std::map scopeFns = {}, std::vector args = {}); +}; + diff --git a/tests/test.funk b/tests/test.funk new file mode 100644 index 0000000..034b89d --- /dev/null +++ b/tests/test.funk @@ -0,0 +1,19 @@ +import("io") + +dingus func { + println("you got dingused") + if (true) { + println("yay") + } + println(321) +} + +main func { + dingus() +} + +infinity func { + while (true) { + println("to infinity and beyond!") + } +} \ No newline at end of file