Code Monkey home page Code Monkey logo

n-lang's Introduction

N

Version Installs Memory Issues Open PRs Open

A programming language by N Building with features such as modular imports.

Install N

Shell (Mac, Linux):

curl -fsSL https://github.com/nbuilding/N-lang/raw/main/install.sh | sh

PowerShell (Windows):

iwr https://github.com/nbuilding/N-lang/raw/main/install.ps1 -useb | iex

Install a specific version

Shell (Mac, Linux):

curl -fsSL https://github.com/nbuilding/N-lang/raw/main/install.sh | sh -s v1.0.0

PowerShell (Windows):

$v="1.0.0"; iwr https://github.com/nbuilding/N-lang/raw/main/install.ps1 -useb | iex

Learn N

See the docs

Python instructions

See python/.

Have something cool in N?

Bugs:

See issues.

JavaScript

The JavaScript version uses Node, TypeScript, and Nearley.

See how to run it in the js/ folder.

Web editor

An IDE is available at https://nbuilding.github.io/N-lang/. It uses the JS version and Monaco, the same editor used in VSCode. The code for the editor is available in the web/ folder.

n-lang's People

Contributors

ashvin-ranjan avatar dependabot[bot] avatar mg27713 avatar saumyasinghal747 avatar sheeptester avatar ytchang05 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

ankitr ytchang05

n-lang's Issues

Keywords can be used as an identifier

This prevents the use of parentheses after return for returning a value (as opposed to calling a function named return).

let return = [let: int] -> int {
  print(let)
}
print(return(3))

This prints 3 then [unprintable value] (Python None)

However, fixing this could break backwards compatibility.

Spread/rest operator

I propose we add a spread operator for lists and record literals. This is a rather large feature, but we can break this down into smaller features and implement them over time, depending on what's needed more.

Syntax

We can compare equivalent for the spread operator for various programming languages:

  • JavaScript: [1, 2, ...array] { a: 1, b: 2, ...object }
  • Python [1, 2, *lyst] { 'a': 1, 'b': 2, **dictionary } (I think)
  • Rust MyStruct { ..MyStruct::default(), a: 1, b: 2 }

I believe Scheme uses . for variable arguments, but that would not fit with N's syntax.

Behaviour

The spread operator in operator precedence could bind looser than prefix unary operators, so ..record.field is allowed (syntax-wise, at least), as is ..cmd!, , and ..function(), but not ..3 ^ 2.

List literals

For list literals, the spread operator can be used to splice a list amongst other items:

[1, 2, ..[3, 4, 5], 6, ..[], 7] = [1, 2, 3, 4, 5, 6, 7]

Obviously, the items in the inner list must match the type of the outer list.

Record literals

The spread operator also works for records.

let record1 = {
  a: true
  b: 2
}

{ ..record1; b: '2'; ..{ c: 3 } }

Notice that the b field does not need to match the type of b from record1. Why? Record fields don't need to be the same type, and b from record1 is guaranteed to be overwritten. Fields can overwrite each other; record fields listed earlier will be replaced by fields of the same name. The type checker can resolve the type of the above record as { a: bool; b: str; c: int }.

This can be used for supplying the default values of a record. For example, { ..defaultOptions, someOption: true }.

Record types

Similarly, the spread operator can be used for types.

alias record1 = {
  a: bool
  b: int
}

alias record2 = { ..record1, b: str, ..{ c: int } }

..{ c: int } is pretty redundant, though, so we might not allow that.

This could also be used in combination with generic types ๐Ÿ˜ฑ:

let myFunction = [[a] record: { ..a, requiredField: int }] -> ...

This can be used kind of like a psuedo-interface, where the record (whether it be a class instance, module, or plain record) must contain at least the specified fields and their types.

List destructuring

In if let and match (see #78), the spread operator can also be used in patterns to destructure the rest of a list:

if let [requiredFirstItem, requiredSecondItem, ..otherItems] = list {

The above conditional pattern requires there to be at least two items, and the rest of the items will be bound in a new list.

We might also allow using the spread operator for the beginning or even middle of the list:

if let [..items, a, b] = list { ... }

if let [a, b, ..items, c, d] = list { ... }

The type of items will be the same as whatever list is.

Record destructuring

Quite like a list, the spread operator can take the remaining fields of a record. Since record fields are guaranteed to exist, they can be used in normal let and other places where variables are bound to a name, like function arguments:

let { field1, field2: newName, ..otherFields } = record

The type of otherFields will be the same as record but without field1 and field2. This can also be used in tandem with generic types.

Unlike the record literal, I don't think the order or placement of the spread operator matters.

For both record and list destructuring, the rest of the items may not be of much use, so they can be discarded with .._.

let { function1, variable1, .._ } = imp "./module.n"

Tuples

We might also allow the spread operator for tuples, both in their literals, types, and patterns.

The spread operator is forbidden elsewhere. This can be enforced either by the type checker or the syntax.

A function that returns an invalid value causes a runtime error during compile time

When a function that has nothing returned is checked it ends up causing something similar to:

display_type was given a value that is neither a string nor a tuple nor a list nor a dictionary nor a NType. None
Traceback (most recent call last):
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\n.py", line 104, in <module>
    error_count, warning_count = type_check(file, tree)
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\n.py", line 49, in type_check
    scope.type_check_command(child)
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\scope.py", line 1829, in type_check_command
    value_type = self.type_check_expr(value)
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\scope.py", line 1333, in type_check_expr
    check_type = self.type_check_expr(argument)
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\scope.py", line 1276, in type_check_expr
    returnvalue = scope.type_check_command(codeblock)
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\scope.py", line 1675, in type_check_command
    exit = self.type_check_command(instruction)
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\scope.py", line 1818, in type_check_command
    display_type(return_type),
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\type_check_error.py", line 92, in display_type
    display += "[%s]" % ", ".join(
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\type_check_error.py", line 93, in <genexpr>
    display_type(typevar, False) for typevar in n_type.typevars
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\type_check_error.py", line 76, in display_type
    "(" + ", ".join(display_type(type, False) for type in n_type) + ")"
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\type_check_error.py", line 76, in <genexpr>
    "(" + ", ".join(display_type(type, False) for type in n_type) + ")"
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\type_check_error.py", line 103, in display_type
    raise TypeError("found None")
TypeError: found None

An example of something that triggered this was:

let pub generateLegalMoves = [p:piece board:list[(int, color)], enPassantSquare:maybe[(int, int)]] -> list[((int, int), bool)] {
	let pieceType, pieceColor = piece
	let out = ""

	if (pieceType == 1) {
		var out = "P"
	} else if (pieceType == 2) {
		var out = "N"
	} else if (pieceType == 3) {
		var out = "B"
	} else if (pieceType == 4) {
		var out = "R"
	} else if (pieceType == 5) {
		var out = "Q"
	}
}

EDIT

After more checking this appears to be the offending code

let board:list[(int, piece.color)] = range(0, 64, 1)
                                                   |> filterMap([_:int] -> maybe[int] { return yes((0, piece.white)) })

here is the enum

type pub color = white
               | black

Trailing commas

The current syntax does not allow for trailing commas.

let personDecoder = D.map2(
	person,
	D.field("name", D.str),
	D.field("height", D.float),
)

The benefit of consistently using trailing commas is that it does not result in the following diffs:

  let coolList: [
    "Billy",
    "Bob",
-   "Joe"
+   "Joe",
+   "Jilly"
  ]

Errors from imported files are counted as one.

This has to do with the way the imported errors are counted as they are 1 object, so multiple errors from an imported file will be displayed as one error putting the count off

Error: You've already defined `Pawn` in this scope.
  --> piece.n:8:11
  8 | class pub Pawn [color:int] {
                ^^^^
Error: You've already defined `Knight` in this scope.
  --> piece.n:30:11
 30 | class pub Knight [color:int] {
                ^^^^^^
Error: You've already defined `Bishop` in this scope.
  --> piece.n:52:11
 52 | class pub Bishop [color:int] {
                ^^^^^^
Error: You've already defined `Rook` in this scope.
  --> piece.n:74:11
 74 | class pub Rook [color:int] {
                ^^^^
Error: You've already defined `Queen` in this scope.
  --> piece.n:106:11
106 | class pub Queen [color:int] {
                ^^^^^
Error: You've already defined `King` in this scope.
  --> piece.n:128:11
128 | class pub King [color:int] {
                ^^^^
Ran with 1 error and 0 warnings

This is a very simple fix though

Make unit tests pass.

Out of the 19 current unit tests, only 7 of them pass, which is way too little for the language to have a functional release if other people are going to use it.

Allow parentheses to be omitted around tuples where sensible

In Python, you don't need parentheses around tuples all the time. For example,

# Swaps variables
a, b = b, a

# Iterate over list with index
for i, item in enumerate(list_of_items_):

# Return a tuple
return None, False

Currently, N requires parentheses around tuple expressions/literals and types (but not patterns, I think). So, currently, the programmer has to write

let a, b: (char, float) = (\{ฯ€}, 3.14)

I feel like those parentheses around the value and type annotation aren't necessary, so instead, one can just write

let a, b: char, float = \{ฯ€}, 3.14

Parentheses will still be required inside type variables, lists, function arguments, and other tuples:

let wow: list[(char, float)] = [(\{e}, 2.72), (\{ฯ€}, 3.14)]

print((1, (2, 3)))

so I don't think allowing their omission should cause any problems.

(I'm adding the Python label because it's not in the Python implementation but it is in the JS implementation as of now; if this feature request gets rejected then I'll have to fix the JS impl.)

Add an update flag to new versions of N

Adding a --update flag to N would help with automatically updating it as it would check for the newest release and if it is different than the one that is on the computer, it would automatically run the update command corresponding to the OS.

Cannot reference type or constructor within class

class Test [] {
  let pub clone = [] -> Test {
    return Test()
  }
}

This will cause two errors one being that the type Test is not defined and the other being that Test() is not defined, this is a problem for many different classes.

Functions that return a cmd[()] require a return statement

Functions that declare their return type as a cmd[()] like so:

let test = [] -> cmd[()] {
	print("test")
}

will cause the type checker to throw this error:

Error: The function return type of a cmd[()] or a () is unable to support the default return of () [maybe you forgot a return].
  --> binary.n:1:26
1 | let test = [] -> cmd[()] {
2 |     print("test")
3 | }
Ran with 1 error and 0 warnings

Even though this should be supported.

JS native module

A very lazy way to interface with outside JavaScript to use N in a front end website or a Node/Deno backend would be to have a native module that can access JavaScript variables. To avoid runtime errors, the module may be very strict, verbose, and inconvenient to use, but libraries written in N can abstract it all away.

Of course, the Python implementation can't feasibly implement JavaScript, so

We can also have a similar module for accessing Python variables, but make a separate issue for that.

Behaviour

During runtime, the behaviour of the js module is implementation defined. ๐Ÿ˜ฑ

  • For the JavaScript impl, it'll just do JavaScript things
  • The Python impl isn't expected to be able to match the JavaScript's impl, which is fine. All the types of the exports for this module are made to allow the Python impl to return err or none for everything. This means that an N project using the js module will still run in the Python impl, but all the results will just fail

Types

The exports here are intentionally minimal. If there's any missing functionality, they can be temporarily worked around using globalThis.eval. The names here are based on Reflect.

import js

// js.value is an exported enum type representing a JavaScript value

// Create JavaScript values
assert type js.string : str -> js.value
assert type js.number : float -> js.value
assert type js.boolean : bool -> js.value
assert type js.array : list[js.value] -> js.value
assert type js.object : map[str, js.value] -> js.value
// The first parameter is the `this` argument; the second is a list of arguments given to the function
assert type js.function : (js.value -> list[js.value] -> js.value) -> cmd[js.value]
assert type js.null : js.value
assert type js.undefined : js.value

// Returns the JavaScript value for globalThis if available
assert type js.globalThis : cmd[option[js.value]]

// Most of these js methods will return a cmd resolving to a result containing a JavaScript value
alias jsResult = cmd[result[js.value, js.value]]

// Calls a function in JS, returning the return value
// Takes the this argument, a list of arguments, then the function value
assert type js.apply : js.value -> list[js.value] -> js.value -> jsResult

// Constructs a constructor, returning the new instance
// Takes a list of arguments then the class
assert type js.construct : list[js.value] -> js.value -> jsResult

// Gets a property from a object
// Takes the property name (a string for convenience, so this doesn't support symbols) then the object
assert type js.get : str -> js.value -> jsResult

// Sets a property of an object
// Takes the property name (no symbols), value, then the object. The result's ok value is ()
assert type js.set : str -> js.value -> js.value -> cmd[result[(), js.value]]

// Determines whether the second value is an instance of the first value
// Takes the class then the instance, and returns a bool from the `instanceof` operator
assert type js.instanceOf : js.value -> js.value -> cmd[result[bool, js.value]]

Type checker does not require returning

It is currently valid to simply do

let isNone = [maybeStr: maybe[str]] -> bool {
  let output = if let <yes _> = maybeStr {
    true
  } else {
    false
  }
}

print(isNone(none))

(I haven't tested this)

The above snippet I believe causes a nasty runtime error because the function returns Python's None.

Finish native libraries.

Many of our native libraries are not finished and are just left with one or two functions, and some test libraries are still out there like fek.

Things to do:

  • Remove fek
  • Allow for bytes in FileIO
  • json
    • Formatting Options
    • Convert Nan and infinities to null
  • request
    • Add Different types of requests
      • HEAD
      • PUT
      • DELETE
      • CONNECT
      • OPTIONS
      • TRACE
      • PATCH
    • Add http servers (as discussed in #147)
  • Allow for console commands and dll use SystemIO
  • times
    • getTime
    • date formatting
  • websocket
    • setInterval
    • Websocket debugging in Command Prompt
    • Websocket server hosting (as discussed in #147)

Async does not work with N executable

This appears to only occur in the executable of N, as it is most likely a problem with pyinstaller or the tool I am using to create the executable

This does occur on Windows, but it is not confirmed yet whether it occurs on the Mac version too.

Here is the error readout

Traceback (most recent call last):
  File "n.py", line 123, in <module>
  File "asyncio\base_events.py", line 642, in run_until_complete
  File "n.py", line 101, in parse_tree
AttributeError: 'Cmd' object has no attribute 'eval'
[9760] Failed to execute script n
Task was destroyed but it is pending!
task: <Task pending name='Task-2' coro=<Function.run.<locals>.run_command() running at function.py:35> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000002339C7EAD30>()]>>
Task was destroyed but it is pending!
task: <Task pending name='Task-3' coro=<Function.run.<locals>.run_command() running at function.py:35> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000002339CBC7970>()]>>

We have deduced that it is an error with the async as this occurred when testing discord.n, which is every async heavy.

Add docstrings

Docstrings are very useful in helping other people understand our code and could help with new contributors.

Issue with using parentheses after an identifier

In the following code snippet,

let main = [a: int] -> int {
  print(a)
  return a * 2
}

let function: int -> int = main

([1, 2, 3] + [4, 5, 6])
  |> len
  |> function
  |> intInBase10
  |> print

This should log 6 then 12. However, it instead gives the following errors:

Error: You haven't yet defined function.
  --> run.n:10:6
10 |   |> function
          ^^^^^^^^
Error: You set function, which is defined to be a int -> int, to what evaluates
to a str.
  --> run.n:6:28
 6 | let function: int -> int = main
 7 |
 8 | ([1, 2, 3] + [4, 5, 6])
 9 |   |> len
10 |   |> function
11 |   |> intInBase10
12 |   |> print
Ran with 2 errors and 0 warnings.

A solution would be to use semicolons:

let main = [a: int] -> int {
  print(a);
  return a * 2;
};

let function: int -> int = main;

([1, 2, 3] + [4, 5, 6])
  |> len
  |> function
  |> intInBase10
  |> print;

Anyone familiar with JS will be familiar with this ASI issue; this is one of the downsides of using this function call syntax

Test-based specification and test syntax

The specification for N should be based on tests rather than a single implementation. This is to ensure that N behaves predictably and the same between implementations and versions of N. This also allows us to document N in N rather than a different language and also make it easier to write documentation of N through examples.

I propose the following types of tests:

Syntax equivalence tests

This already exists in tests/syntax. It's essentially just a bunch of code snippets separated by three newlines. Each snippet must parse without error and be equivalent to each other; that is, it should ignore redundant parentheses and whitespace. This way, we can test to ensure that, say, order of operations is enforced in the same way across implementations.

It's important to note that type checking isn't done at this stage.

Syntax error tests

Sometimes, we want to ensure that certain syntax remains invalid, at least for now. These tests are guaranteed to fail, and if the implementation parses it correctly, the implementation is incorrect.

Type checking tests

There should also be a way to ensure that a type check error or warning is given for a code snippet. Perhaps this could just be merged with the syntax error tests.

For example, we want to ensure that

let myFunc = [_: ()] -> int {
  let a = 2
}

is not valid. This should give an error about not returning anything. However, it might be hard to specify specifically what kind of error should be raised and where since errors are currently not intended to be the same across implementations (๐Ÿ‘‰๐Ÿ‘ˆ unless? ๐Ÿฅบ).

Type tests

For native functions and native library functions, it might be nice to easily document their types in plain N syntax.

I propose an assert type statement. assert is not a reserved word, but since two identifiers can't appear next to each other in normal N code, this should be fine as long as we prohibit a newline between the two words. This takes an expression followed by a colon followed by a type signature.

For example, for the json library, we can do

import json

assert type json.parse : str -> json.value
assert type json.parseSafe : str -> maybe[json.value]
assert type json.stringify : json.value -> str

Perhaps in the future, we can use this to automatically generate documentation for native functions!

Runtime tests

We can should also test that the functions return proper values.

I propose an assert value statement. Similarly, no newlines are allowed between assert and value for backwards compatibility. This takes an expression that should evaluate to a boolean.

For example,

import json

assert value 2 + 2 = 4
assert value json.parseSafe("{") = none
assert value json.parseSafe("\"hello\"") = string("hello")

Each implementation can then design a test suite to run all of these tests to ensure new features do not break old ones. If an implementation does not pass a test, the implementation has a bug.

These tests can also be used for libraries written in N to document examples and ensure their libraries work as expected!

All future feature requests to N must provide tests, and anything not tested is formally not part of N and not protected by backwards compatibility.

Tests for assert value and assert type

Since these two are the only actual additions to N, I'll try to demonstrate what an ideal feature request to N will look like.

Syntax equivalent tests

// These should just parse successfully; remember that type checking isn't performed for these tests!
assert type 3:str, int -> maybe
[str, int]

assert   value
3

Syntax error tests

assert
  type 3 : int
assert
value 4

Type checking tests

// ok
assert type 3 : int

// err: type mismatch (currently error messages aren't standardised, so the error message here is implementation-defined)
assert type 3 : str

// QUESTION: What should the result of this be?
assert type [] : list[str]

// ok
assert value false

// err: assert value should be given a boolean
assert value 3

Type tests

Not applicable because this only introduces new syntax.

Runtime tests

It's hard to do a runtime test for the runtime test syntax. The following are runtime tests for existing functions to both test them and assert value:

assert value intInBase10(-0) = "0"
assert value round(1.0 / 0.0) = 0
assert value charCode(\{๐Ÿ‘}) = 55357
assert value substring(-500, -1034, "happy") = ""
// Note that `len` may have platform dependent behaviour; this might be a bug that should be stamped out through tests
assert value len(3) = 1
assert value range(0, 0, 300) = []
// `type` also has a non-standardised return value
assert value type(3) = "int"
assert value print({ a: 3 }) = { a: 3 }

Else branch with conditional let allows undefined variables

Minimal example:

if let <yes test> = none {
	print(test)
} else {
	print(test)
}

This causes a runtime error:

...
  File "...\N-lang\python\scope.py", line 162, in get_
variable
    raise NameError("You tried to get a variable/function `%s`, but it isn't def
ined." % name)
NameError: You tried to get a variable/function `test`, but it isn't defined.

I suspect that the type checker uses the same scope for both the if and else branch, so the variables bound by the conditional let seem to be available in the else branch by the type checker.

Error Displayer displays class types as records

This occurs in situations like this:

class Bit [value:int] {
	let internal = value % 2
	let pub toString = [] -> str {
		return intInBase10(internal)
	}

	let pub toInt = [] -> int {
		return internal
	}
}
let b = Bit("test")

When checking the program outputs this:

Error: int -> { toString: str; toInt: int }'s argument #1 should be a int, but you gave a str.
  --> binary.n:37:13
37 | let b = Bit("test")
                 ^^^^^^
Ran with 1 error and 0 warnings

This should be an easy fix though

request.get returns record with a feild called return

This is a problem because as of fixing #80 getting this field will cause a syntax error.

As an example, this is taken from the identify branch if discord.n:

let pub getMessage = [c:str m:str] -> cmd[maybe[message.Message]] {
	let url:str = "https://discord.com/api/channels/" + c + "/messages/" + m
	let r = request.get(url, header)!

	if r.code /= 200{
		return none
	}

	return parseMessageData(r.return)
}

And this will end up creating a syntax error on return parseMessageData(r.return)

Proper mutability in N

The problem

Currently, all variables can be changed using var, which is very unsafe. Oftentimes we write code assuming our variables won't suddenly change; for example, we assume that intInBase10 will always be the same intInBase10. However, if any variable can be var'd, this assumption can no longer be made.

Also, var's behaviour is ill-defined in some edge cases like classes, records, and function arguments, and requires that the execution order of presumably pure functions be defined. For example, it should not matter whether a third party tool peeks into a function's return value or not.

Here's why mutable variables (and impure functions) suck:

  • When debuggng, printing an impure function to see its output can unexpectedly change the behaviour the program.
  • Third party tools cannot preview or peek into return values of functions without causing some other side effect.
  • Async tasks running in parallel become unstable. For example, var count = count + 1 might be an issue if, while adding count + 1, another thread sets count to a new value. Then, count + 1 reflects a sum with the old value and is set to count, which overrides the first change.

Possible solution: let var

For backwards compatibility, existing var statements will just emit warnings. This is fine; the JS branch already intended on marking var as undefined behaviour.

Perhaps we can do

let var mutableVariable = { count: 0 }

where var here indicates that the variable is mutable (or variable, as opposed to constant).

Then, the variable is free to be mutated without warnigns:

var mutableVariable = { count: mutableVariable.count + 1 }

mutableVariable is usable like a normal immutable value:

let immutable: { count: int } = mutableVariable

This makes a deep copy of the mutable value, so changing mutableVariable won't affect the value of immutable.

Now, let's take an example which currently has undefined/ill-defined behaviour:

let makeCounter = [_: ()] -> () -> int {
  let count = 0
  return [_: ()] -> int {
    var count = count + 1
    return count
  }
}

let counter = makeCounter(())
print(counter(())) // 1
print(counter(())) // 2
print(counter(())) // 3

counter violates expectations. Despite giving the same value (), it returns a different value each time. This will harm debugging; trying to print a function call might change the behaviour of a program.

One solution would be to use a cmd. However, the problem with cmds is that they can only be used inside other cmd functions. A function using makeCounter can still be pure. For example,

let addFirstIntegers = [upTo: int] -> int {
  let sum = 0
  let counter = makeCounter(())
  for (_ in range(0, upTo, 1)) {
    var sum = sum + counter(())
  }
  return sum
}

Despite using var and an impure function, on the outside, addFirstIntegers is pure. Giving it the same value for upTo will always make it return the same output.

Ideally, there should be a way to denote functions that are impure but can be safely used inside pure functions. Perhaps this can be done as a fancy var mutable type, like var int for a mutable int.

Another case might be to pass mutable pointers to other functions. For example, we might want a remove function that modifies a mutable list.

let var mutableList = [1, 2, 3]
removeItem(1, mutableList) // Removes second item (index 2) from list

With what I've said above, this currently would make a copy of mutableList, which is slow and won't mutate the mutableList. Ideally, removeItem should be able to modify mutateList from inside the function.

Summary of issues

let var solves one problem, but I list two more problems that it can't solve alone:

  1. Denoting impure functions that can be used in a pure function (such as counter)
  2. Making functions that can mutate a given mutable value

Let's discuss some possible solutions to those or alternatives to let var here. Hopefully with these improvements to mutability, we can make N a much more pleasant and reliable language to use.

Type generics not getting cast when uninitialized

Type generics are a bit scuffed as

assert type map[str, str] = mapFrom([])

and

assert type [a, b] (a -> b) -> list[a] -> list[b]: [x, y] (x -> y) -> list[x] -> list[y]

fail even when they should be cast.

Syntactical ambiguity when assigning a variable to a pipe output in a function

let main = [_: ()] -> () {
    let results: list[result[int, str]] = [err("sad"), ok(2), err("wow")]

    let errors = filterMap(
        [result: result[int, str]] -> maybe[str] {
            return if let <err error> = result {
                yes(error)
            } else {
                none
            }
        },
        results
    )
        |> err

    print(errors)
}

main(())

The above code inconsistently alternates between logging a function (presumably filterMap) and <err ["sad", "wow"]>.

image

Adding a type annotation result[int, list[str]] can produce the following error:

Error: You set errors, which is defined to be a result[int, list[str]], to what
evaluates to a (a -> maybe[b]) -> list[a] -> list[b].
  --> run.n:4:42
 4 |     let errors: result[int, list[str]] = filterMap(
                                              ^^^^^^^^^
Ran with 1 error and 0 warnings.

Allowing for API usage of N

API Usage

What I am proposing is that instead of automatically running the program and outputting the errors to console, we allow n.py to be imported and used to parse files and return the errors that it has found, sort of like an API, here is an example of how that would work:

import n
out = n.parse_text("let i:int = \"Oh, no, I cause an error\"") # out can maybe be a Scope
for i in out.errors:
  print("oops!")

This can be used to great effect in IDEs as they can now run compile-time and catch errors quickly as opposed to having to modify the code present on GitHub every update to fit their needs.

Implementation

The way that this can be implemented is very simple in the python branch, we can simply do:

if __name__ == "__main__":

This will stop the code in the if statement from running if the program was imported, allowing for it to be used as an API for other projects, overall increasing the usefulness of N, as if it is compatible with more IDEs, more people will use it because it is easier to use.

Calling a curried constructor results in an error

When calling a curried constructor like so:

class SomeClass [a:int b:int] {
  // some code
}

let one = SomeClass(1)
let classInstace = one(4)

it will cause this error at runtime:

Traceback (most recent call last):
  File "n.py", line 84, in <module>
  File "asyncio\base_events.py", line 642, in run_until_complete
  File "n.py", line 56, in parse_tree
  File "scope.py", line 692, in eval_command
  File "scope.py", line 660, in eval_command
  File "scope.py", line 701, in eval_command
  File "scope.py", line 602, in eval_expr
  File "scope.py", line 623, in eval_expr
  File "scope.py", line 602, in eval_expr
  File "scope.py", line 496, in eval_expr
  File "scope.py", line 548, in eval_expr
  File "scope.py", line 602, in eval_expr
  File "scope.py", line 497, in eval_expr
  File "scope.py", line 619, in eval_expr
TypeError: 'NConstructor' object is not subscriptable

Implement custom stack traces and runtime errors.

For runtime errors that we are unable to avoid (such as StackOverflow errors) or Keyboard interrupts, it is best to have our own trace for the N program that was running instead of the internals. The errors that occur when OMG AN ERROR is printed out should stay how is though.

Feature: Add `match`

Many good programming languages have something like match: Elm, Rust, Kotlin

match takes in a value and you specify different matching patterns; this is mainly used for enums but can also be used for matching strings and numbers.

An example with enums (in this case, the built in result type)

let wow: result[int, str] = ...
let lol: int = match wow {
  <ok number> -> number
  <err reason> -> len(reason)
}

An example with strings

print(match SystemIO.inp("Ask me a question.")! {
  "Who are you?" -> "I am an example."
  "How are you?" -> "I am good."
  "Why are you?" -> "Why are *you*?"
  _ -> "I don't understand."
})

Because match is an expression, it must return a value. Thus, a comprehensive check must be performed to ensure that every case is accounted for.

`parallel` function for running `cmd`s in parallel

I think it'd be nice to be able to take advantage of the asynchronousness of cmds by being able to execute another cmd in parallel.

Perhaps we can add a function parallel:

assert type parallel : [t] cmd[t] -> cmd[cmd[t]]

import time
let func1 = [] -> cmd[()] {
  time.sleep(1)!
  print("One second later")
}
let func2 = [] -> cmd[()] {
  time.sleep(2)!
  print("Two seconds later")
}
let pub main = [] -> cmd[()] {
  let func1Done = parallel(func1())!
  let func2Done = parallel(func2())!
  
  func1Done!
  func2Done!
}
  |> ()

This will wait a second, print One second later, wait another second (rather than two seconds), then print Two seconds later.

You give parallel a cmd, and once you await it with !, it'll start executing the given command in parallel. The cmd it returns, when awaited, will resolve once the given cmd finishes executing. If you await it after the cmd already finished, it'll resolve immediately.

Of course, there is no obligation to use the return value of parallel; you can let it run in parallel and not bother about it.

parallel(otherCmd)!

This can be used to implement setInterval (by using while true + time.sleep). Also, the nice thing about N is that most things are pure, so this can be used for multithreading, though currently communication between parallel cmds is limited. That'll have to go in a separate PR (what if we added Mutex to N ๐Ÿ‘€)

Else if does not work, and if statements cannot be stacked.

The main problem is that else if does not work as it will throw the error

Error: Internal problem: I only deal with instructions, not if.
--> test.n:1:16
1 |         } else if true {
2 |             // some code
3 |         }
Ran with 1 error and 0 warnings

and stacking if statements like so:

if false {
  // some code
} if true {
  // some code
}

will cause a syntax error because if is a command, so there needs to be a newline or ; separating them

Consider function, if/else, and match expressions as values rather than an expression

In the syntax, values are generally more tautly contained, like numbers or record literals, where you can't possibly think of ripping the value up into smaller pieces. Expressions, however, usually consist of other expressions that could be "ripped up"; for example, 3 + 3 in some contexts (like next to a multiplication * operator) can be rebracketed if no parentheses are used. Expressions can be used as a value by putting them in parentheses, which in effect keeps the expression contained.

As a rule of thumb, you need parentheses if you want to use an expression in multiplication, but you don't for a value:

let prod1 = 3 * "wow" // No parentheses needed around a string; it's a value!
let prod1 = 3 * 1 + 1 // `1 + 1` isn't a proper grouping here because multiplication binds tighter (per PEMDAS); it's an expression!

For reference, here are the expressions and values in the Python and JS implementations:

N-lang/python/syntax.lark

Lines 140 to 145 in 61bb3e4

?expression: ifelse_expr
| boolean_expression
| function_def
| anonymous_func
| function_callback_pipe
| match

N-lang/python/syntax.lark

Lines 174 to 188 in 61bb3e4

value: NUMBER
| BOOLEAN
| STRING
| NAME
| "(" expression ")"
| UNIT
| function_callback
| char
| tupleval
| listval
| recordval
| impn
| HEX
| BINARY
| OCTAL

expression -> tupleExpression {% id %}
| returnExpression {% id %}
| "imp" _ identifier {% from(ast.ImportFile) %}
| "imp" _ string {% from(ast.ImportFile) %}

value -> identifier {% id %}
| %number {% from(ast.Number) %}
| %float {% from(ast.Float) %}
| string {% id %}
| %char {% from(ast.Char) %}
| "(" _ ")" {% from(ast.Unit) %}
| "(" _ expression _ ")" {% includeBrackets %}
| ("[" _) ((noCommaExpression (_ "," _)):* noCommaExpression ((_ ","):? _)):? "]" {% from(ast.List) %}
| ("{" _) ((recordEntry blockSeparator):* recordEntry (blockSeparator | _spaces)):? "}" {% from(ast.Record) %}

(Function expressions are listed as pipeRhs in the JS impl. at the same level as booleanExpression, so it and if expressions are effectively expressions)

There's notable differences between the implementations so we'll have to write tests documenting the correct behaviour and then fix each implementation accordingly. (The Python branch is no longer the de facto implementation per #117 and might be incorrect.) That's a separate issue though!

Anyways, it would be nice if we could use function expressions, if/else expressions, and match expressions without parentheses. After all, they look fairly self-contained.

let wow = 3 * if true { 9 } else { 6 }

That can't be rebracketed as 3 * if because (3 * if) true { 9 } else { 6 } isn't valid syntax.

Parentheses are quite annoying, so it would be nice to not have to use them.

This should be a fairly simple change, just moving those expressions from expression to value. Assuming the tests are complete, they shouldn't fail more than they did before with this change.

  • For function expressions, ignoring type errors, 3 * [] -> () {} * 3 isn't syntactically ambiguous, even if the [] could be an empty list and the {} could be an empty record. -> is only used in function expressions and type annotations (and I guess match expressions?), and type annotations are fairly isolated from expressions. Thus, the -> is fairly indicative of a function expression, so there's no way to ambiguously reanalyse un-parenthesised function expressions

  • For if/else expressions, we don't allow values right next to each other with only spaces in between. For example, wow { a: 1 } sheep { b: 2 } isn't valid syntax. Even with newlines in between, because we have a limited subset of expressions that are allowed as statements, they're still invalid syntax. So, 3 * if condition { a } else { b } * 3 isn't ambiguous. Even if it got rebracketed to 3 * if and { b } * 3 (again, ignoring type errors), the middle condition { a } else is a series of values without anything in between them, so that's invalid syntax. Even with newlines, they aren't valid statements on their own:

    // Not ambiguous
    let wow = 3 * if
    condition
    { a }
    else
    { b } * 3

    Also, if and else are keywords, so they're quite certainly for an if/else.

  • Similarly for match expressions, 3 * match(value) { ... } * 3 could be seen as 3 * match(value) and { ... } * 3, but they're next to each other, so that's invalid syntax. Additionally, match is also a keyword

Suggested features to add from Kotlin

Kevin has compiled a gist of all of the things that are interesting in Kotlin that we may adopt

  • Inferring
  • Ranges
    • .. operator
    • until operator
    • step operator
    • downTo operator
  • in operator
  • Destructuring
  • Destructuring in for loops
  • when statement (see #161)
  • match statement (see #78)
  • to operators for tuples
  • Expressions able to be evaluated everywhere
  • if, else, and else if can all be expressions
  • Primary constructors
  • Contructors able to be defined in classes
  • == does not compare memory location
  • vararg or something similar

The - sign cannot be applied to variables

while -1 does work in N something like:

let test = 3
print(-test)

will throw a compile-time error because it does not know how to apply - to a variable, so it requires the programmer to use 0-test instead

split() does not work correctly.

split does not work as the arguments are swapped around in the internals of the functions, leading to it outputting the opposite of what is wanted.

Unexpected EOF does not get printed out correctly

An unexpected EOF error thrown by lark during parsing displays as this:

Traceback (most recent call last):
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\n.py", line 76, in <module>
    tree = file.parse(n_parser)
  File "C:\Users\Ashvin Ranjan\Desktop\N\main\python\file.py", line 22, in parse
    return parser.parse('\n'.join(self.lines))
  File "C:\Users\Ashvin Ranjan\AppData\Local\Programs\Python\Python39\lib\site-packages\lark\lark.py", line 517, in parse
    return self.parser.parse(text, start=start)
  File "C:\Users\Ashvin Ranjan\AppData\Local\Programs\Python\Python39\lib\site-packages\lark\parser_frontends.py", line 112, in parse
    return self.parser.parse(text, start)
  File "C:\Users\Ashvin Ranjan\AppData\Local\Programs\Python\Python39\lib\site-packages\lark\parsers\earley.py", line 306, in parse
    raise UnexpectedEOF(expected_terminals, state=frozenset(i.s for i in to_scan))
lark.exceptions.UnexpectedEOF: Unexpected end-of-input. Expected one of:
        * SUBTRACT
        * EXPONENT
        * LESS
        * LPAR
        * SEMICOLON
        * NEQUALS
        * SEMICOLON
        * MULTIPLY
        * MODULO
        * LPAR
        * __ANON_2
        * DOT
        * GORE
        * OR
        * GREATER
        * DIVIDE
        * AWAIT
        * AND
        * NEWLINE
        * LORE
        * LPAR
        * ADD
        * EQUALS
        * RBRACE

Scopes inside functions require return

When using any sort of code that creates a new scope inside a function scope, the type checker requires a return such as:

class Byte [value:list[Bit]] {
	let pub toString = [] -> str {
		let out = ""
		for (bit in value) {
			var out = out + bit.toString()
		}
		return out
	}
}

Will cause the type checker to throw

Error: The function return type of a str is unable to support the default return of () [maybe you forgot a return].
  --> binary.n:15:28
15 |         for (bit in value) {
16 |             var out = out + bit.toString()
17 |         }
Ran with 1 error and 0 warnings

This also occurs with if statements

`[]` in N

I have recently been informed that the way to access a list at an index in N is

let x = list |> itemAt(int) |> default([])

This is obviously bad and terrible and bad. For reference, most languages have it like

let x = list[int] ?? []

which I believe to be much cleaner, especially for something that comes up as often as getting the index of a list.
If you want to be even cooler, you can be like kotlin and have [] be an overload for .get() similar to how + is an overload for .plus() and == for .equals(), etc. which allows it to work for maps and other data structures.

val x = mapOf("blah" to "wow")
val y: String? = x["blah"]
val z: String? = x["wow"]

You can keep the current behavior of returning a maybe[] as well.

Add `subsection` and fix `append`

subsection

subsection will work by taking in two ints and a list and returning the subsection of that list.

append

For some reason append is not a void function, which is slightly annoying

While loops and accompanying features

While loops

An important feature missing from N for too long has been the while loop. While it may not have too many useful functionalities, now that the for loop has started iterating through lists, it is important that this feature is to be added. There are many ways of implementing it but I will currently suggest this:

import SystemIO
let name = ""
while (~validName(name)) {
  var name = SystemIO.inp("Please input a name")
}

This would allow for easy usage of the while loop that is familiar to those that do not know N that well, as it imitates the way that other programming languages use while loops.

Continue

continue is a keyword already reserved in the fixing of #80 along with while and break. It would be added in a way that one can just call it like a simple statement as such:

for (i in range(0, 10, 1)) {
  if (i == 1) {
    continue
  }
  print(i)
}

This would be a simple, and familiar way of using continue to most people. It should be easy to implement as it is similar to return but with less error checking.

Break

break is a very important keyword that for some reason has been left unimplemented for a long time, yet it reserved as a keyword when #80 was fixed. The way break would work is that it would be called in a similar way to continue except instead of continuing to the next iteration, it would break out of the loop entirely, this could also be used in if statements as it might be very helpful. There are some foreseeable issues with scoping break as in the situation.

if (1 == 1) {
  if (true) {
    break
  }
  if (true) {
   print("hi")
  }
}

it may be that the user does not want to exit out of the first if statement of vice versa, in this situation it is hard to accurately judge the scope of the break.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.