Skip to content

Instantly share code, notes, and snippets.

@IAmAnubhavSaini
Created July 12, 2024 18:45
Show Gist options
  • Save IAmAnubhavSaini/f44d52a485159adac1ff076bc4ec5f96 to your computer and use it in GitHub Desktop.
Save IAmAnubhavSaini/f44d52a485159adac1ff076bc4ec5f96 to your computer and use it in GitHub Desktop.
Draft 4 for a new c

Draft 4 of a-new-c


/*! type Rectangle
  * defines a type named Rectangle; this would be like a class in many other languages
  * defines a complete implementation as per the spec/ideas today
  * */
type Rectangle {
	//! @data defines the data for this type
	@data {
		/*! the complete syntax would be
		  * Number length = 0 "rectangle.length"
		  * which signifies that when sync, or converted to database string or csv or json
		  * the key length will be named as rectangle.length in title/field
		  * but since, it is defaulted to typename.toLower() dot field.toLower()
		  * we didn't have it here...
		  * */
		Number length = 0
		Number width = 0 "rectangle.otherLength"
	}

	/*! @type
	  * this defines the functions that are defined on the type itself
	  * think static in most languages
	  * these will be accessible via Rectangle dot in this case
	  * Rectangle.new(10)
	  * When writing functions on types, do not depend upon other functions in @type.
	  * 
	  * */
	@type {
		/*! new(Number side)
		  * new is straight forward, it creates an instance of the type
		  * it is defined in
		  * */
		new(Number side) -> Rectangle{length: side, width: side}
		/*! new
		  * overloaded function with two input values this time
		  * */
		new(Number length, Number width) -> Rectangle{length, width}
		/*! default
		  * default values are big things in a-new-c
		  * every value has a default value
		  * since length and width already have default values,
		  * we actually don't need this function here
		  * it is here only for documentation as if not defined default function
		  * Rectangle.default would anyway boot a Rectangle with default values
		  * */
		default -> Rectanle{length:0, width: 0}
		/*! from(Square s)
		  * from is an overloadable function that handles convertibility
		  * from different values to a Rectangle
		  * Here it is creating a rectangle from a give square s
		  * */
		from(Square s) -> Rectangle{length: s.side, width: s.side}
		from(Cuboid c) -> Rectangle{length: c.length, width: c.length}
		from(Number n) -> Rectangle{length: n, width: n}
		from(Integer i) -> Rectangle{length: i, width: i}
		/*! from(String s)
		  * this one actually is stupid and makes no sense
		  * but hey you can do it in this lang
		  * */
		from(String s) -> Rectangle{length: s.length, width: s.length}
	}
	/*! @instance r
	  * This one basically delineates the functions that are available on 
	  * the instance of this type i.e. Rectangle
	  * Think of these as methods, and r is the reciever object name
	  * */
	@instance r {
		/*! area
		  * It is actually a function.
		  * You don't need empty parenthesis around functions if they don't
		  * take any input, e.g. here it depends on the internal state 
		  * You don't need empty parenthesis when calling a function that doesn't take
		  * any input. so the call would be r.area
		  * and it is still a function call
		  * And the function works only on two values it already knows the type of,
		  * so it doesn't need the type :Number to be marked.
		  * */
		area -> r.length * r.width
		/*! perimeter:Number
		  * Again a function, but specifies return type.
		  * */
		perimeter:Number -> 2 * (r.length + r.width)
		/*! compareByArea(Rectangle other): `larger | `smaller | `equal -> {
		  * 
		  * Notice that the type is passed as the TypeName and not as type Rectangle.
		  * 
		  * Notice the things that start with a backtick i.e. `
		  * These are atoms. These are unique for every runtime, every program, 
		  * i.e. `larger will always be equal to `larger from other program/processes...
		  * Notice -> {
		  * A function can be x -> x * x or x -> { return x * x }
		  * */
		compareByArea(Rectangle other): `larger | `smaller | `equal -> {
			/*! return match
			  * match resolves in a value.
			  * here the input for match clause is empty, that means,
			  * match will try to process the conditions below, and
			  * whiever is true first, will be evaluated.
			  * */
			return match {
				//! this uses => syntax because it is not a function
				//! only a conditional
				//! the syntax is conditional => value
				//! think of it as return value if condition == true
				r.area < other.area => `larger
				r.area > other.area => `smaller
				//! special case, think default from switch
				_ => `equal
			}
		}
		compareByPerimeter(Rectangle other): `larger | `smaller | `equal -> {
			return match {
				r.perimeter > other.perimeter => `larger
				r.perimeter < other.perimeter => `smaller
				_ => `equal
			}
		}
		scale(Number factor):Rectangle -> Rectangle.new(r.length * factor, r.width * factor)

		/*! to(type T)
		  * to is a function that will accept a type and variabalize it as T
		  * it then returns the object of the specific requested type
		  * This basically means we can convert current object which is a rectangle
		  * to other known objects as per our knowledge and public shapes and interfaces
		  * the call to to() would look like s := r.to(Square)
		  * */
		to(type T) -> {
			//! T is not a variable, but a variable that contains type, thus
			//! it is marked as type T
			return match type T {
				//! to make sure that match works, we need to use type Name
				type Square => Square.new(side: Number.min(r.length, r.width))
				type String => "Rectangle{length: $(r.length), width: $(r.width)}"
			}
		}

		/*! Here be dragons
		  * Why should function names be only alphanum?
		  * If it makes sense, use symbols as function names
		  * Check operators below to see how these will/can be called
		  * */
		+(Rectangle other) -> Rectangle.new(r.length + other.length, r.width + other.width)
		-(Rectangle other) -> Rectangle.new(r.length - other.length, r.width - other.width)
		*(Rectangle other) -> Rectangle.new(r.length * other.length, r.width * other.width)
	}

	/*! @operator
	  * I like these; and these are not functions
	  * Depending upon number of args, there are unary and binary operators.
	  * How do you implement + operator for three values?
	  * */
	@operator {
		//! unary
		+(Rectangle r) -> Rectangle.new(Number.abs(r.length), Number.abs(r.width))
		//! binary
		+(Rectangle r, Rectangle o) -> r.+ o
		-(Rectangle r, Rectangle o) -> r.- o
		*(Rectangle r, Rectangle o) -> r.* o
	 }
// I am ommitting tests from this type
// because I do not want to throw everything at once at you...
}


/*! @pool(100)
  * it will carve out memory for 100 square objects 
  * at the program boot
  * slow init, but will allocate faster.
  * Advanced, will be implemented later.
  * */
@pool(100)
type Square {
	@data {
		Number side: 0
	}
	@type {
		new(Number n) -> match {
			//! Notice we are not returning Just(Square... 
			//! because Just resolves in Type anyway
			n >= 0 => Square{side: n}
			//! Nothing of type turns all of the methods, functions to nothing
			//! and it will return Nothing() of the expected type
			//! e.g. Nothing(square).area will be Nothing(Number)
			_ => Nothing(Square)
		}
		default -> Square{side: 0}

		//! from notice logic difference wrt Rectangle.to(Square)
		from(Rectangle r) -> Square{side: r.length}
		//! notice how area cannot be negative, but still number can be
		//! this is one of the reasons that makes the programming difficult and error prone
		//! the assumption is hidden away...
		//! fix it with match like in new above
		fromArea(Numebr n) -> Square{side: Maths.sqrt(n)}
	}

	@instance s {
		area -> s.side * s.side
		@test area {
			t("check if area can be 0", _ -> expect(Square(0).area).toEqual(0))
			t("default area is 0", _ ->  expect(Square.default).area).toEaqla(0))
			//! not how area doesn't return Nothing, new returns Nothing(Square)
			//! area just returns Nothing(Number) because that's the type area returns
			t("area is nothing when sides are negative", _ -> {
				q := Square.new(-1)
				expect(q.area).toBe(Nothing(Number))
			})
		}
		perimeter -> 4 * s.side
		@test perimeter {
		...
			/*! at this point you might chime in that this is too
			  * much for a file or even type to have.
			  * But I say, no.
			  * The Documentation, The Code, and the Test belong together.
			  * In addition, the production binary will not be built if
			  * - tests don't pass
			  * - documentation for public types and functions is not provided
			  * You might say that this is a lot of noise.
			  * I say, with LSP, you will have dimmed other two. So that you can work
			  * on the current one that has your focus.
			  * For pure text/note apps without LSP and color coding: Yeah! 
			  * Please come to latest century
			  * For accessibility, editors/lsp will also provide 
			  * jumps between doc, test, and code: doc -> test ...
			  * and also from one test to next, test -> test,
			  * and also from tests of one function to next
			  * haev to, right?
		...
		}
		/*! compare
		 * alternatively, keeping the signature same...
		 * -> match { ... }
		 * Because the match results in a value/expression itself, 
		 * we can just return that
		 * also
		 * all compare functions would look like this in every type
		 * please do not suggest DRYing it out.
		 * complex objects might need these
		 * also, there is no default compare or interface
		 * so gotta implement these if you want sort to work with your objects
		 * Also you can compare with other objects, because function overloading is possible
		 * */
		compare(Square other): `larger | `smaller | `equal -> {
			return match {
				s.area < other.area => `smaller
				s.area > othe.area => `larger
				_ => `equal
			}
		}
	

		scale(Number factor) -> Square.new(s.side * factor)

		to(type T) -> match type T {
			type Rectangle => Rectangle.new(s.side, s.side)
			type String => "Square{side: $(s.side)}"
		}
	}

	@operator {
		+(Square s, Square o) -> Square.new(s.side + o.side)

		/*! @mutate 
		  *	mutates input object/variables in memory
		  * s := Square.new(10)
		  * 0s 
		  * s has side of 0 now.
		  * I get kind of horror it would be to implement, and
		  * the compiler would have to go through the code
		  * multiple times.
		  * But it would be more expressive in what you want to say/do
		  * and only that matters. Also, these are function-ish??
		  * */
		@mutate
		0(Square s) -> {
			s.side = 0
		}
	}
}

Same code without comments


type Rectangle {
	@data {
		Number length = 0
		Number width = 0 "rectangle.otherLength"
	}

	@type {
		new(Number side) -> Rectangle{length: side, width: side}
		new(Number length, Number width) -> Rectangle{length, width}
		default -> Rectanle{length:0, width: 0}
		from(Square s) -> Rectangle{length: s.side, width: s.side}
		from(Cuboid c) -> Rectangle{length: c.length, width: c.length}
		from(Number n) -> Rectangle{length: n, width: n}
		from(Integer i) -> Rectangle{length: i, width: i}
		from(String s) -> Rectangle{length: s.length, width: s.length}
	}
	@instance r {
		area -> r.length * r.width
		perimeter:Number -> 2 * (r.length + r.width)
		compareByArea(Rectangle other): `larger | `smaller | `equal -> {
			return match {
				r.area < other.area => `larger
				r.area > other.area => `smaller
				_ => `equal
			}
		}
		compareByPerimeter(Rectangle other): `larger | `smaller | `equal -> {
			return match {
				r.perimeter > other.perimeter => `larger
				r.perimeter < other.perimeter => `smaller
				_ => `equal
			}
		}
		scale(Number factor):Rectangle -> Rectangle.new(r.length * factor, r.width * factor)

		to(type T) -> {
			return match type T {
				type Square => Square.new(side: Number.min(r.length, r.width))
				type String => "Rectangle{length: $(r.length), width: $(r.width)}"
			}
		}

		+(Rectangle other) -> Rectangle.new(r.length + other.length, r.width + other.width)
		-(Rectangle other) -> Rectangle.new(r.length - other.length, r.width - other.width)
		*(Rectangle other) -> Rectangle.new(r.length * other.length, r.width * other.width)
	}

	@operator {
		+(Rectangle r) -> Rectangle.new(Number.abs(r.length), Number.abs(r.width))
		+(Rectangle r, Rectangle o) -> r.+ o
		-(Rectangle r, Rectangle o) -> r.- o
		*(Rectangle r, Rectangle o) -> r.* o
	 }
}

@pool(100)
type Square {
	@data {
		Number side: 0
	}
	@type {
		new(Number n) -> match {
			n >= 0 => Square{side: n}
			_ => Nothing(Square)
		}
		default -> Square{side: 0}

		from(Rectangle r) -> Square{side: r.length}
		fromArea(Numebr n) -> Square{side: Maths.sqrt(n)}
	}

	@instance s {
		area -> s.side * s.side
		@test area {
			t("check if area can be 0", _ -> expect(Square(0).area).toEqual(0))
			t("default area is 0", _ ->  expect(Square.default).area).toEaqla(0))
			t("area is nothing when sides are negative", _ -> {
				q := Square.new(-1)
				expect(q.area).toBe(Nothing(Number))
			})
		}
		perimeter -> 4 * s.side
		@test perimeter {}
		compare(Square other): `larger | `smaller | `equal -> {
			return match {
				s.area < other.area => `smaller
				s.area > othe.area => `larger
				_ => `equal
			}
		}
	

		scale(Number factor) -> Square.new(s.side * factor)

		to(type T) -> match type T {
			type Rectangle => Rectangle.new(s.side, s.side)
			type String => "Square{side: $(s.side)}"
		}
	}

	@operator {
		+(Square s, Square o) -> Square.new(s.side + o.side)
		
		@mutate
		0(Square s) -> {
			s.side = 0
		}
	}
}

Some functions to show difference/commonality

Hello world

// hello.nuc

hello(String what): String -> "hello, $(what)!"
start -> hello("a-new-c")

// build
nucc --type=production --binary-name=hello --architecture=amd6 build --files=hello.nuc

./hello
hello, a-new-c!

I am totally eyeballing these, since there is no compiler in sight so far.

Two sum

scanUntil works until the argument function returns true for first of many or only output When it finds true return, it stops and returns those values back as the result of scanUntil. When it doesn't, it keeps running till the end, and just returns the returned value or init value

As you'll notice, the compiler would have to go through each such instance and build ad hoc types; which I must repeat myself, is required for developer's sanity. All this because the lang doesn't have break, continue, and goto.

twoSum(Integer[] nums, Integer target): Integer[] {
	Integer[] diffs := []
	
	found, out := nums.scanUntil(value, index -> {
		found, foundIndex := diffs.contains(value)
		diffs.push(target - value) if not found otherwise keep going
		
		return match {
			found && index != foundIndex => found, [index, foundIndex]
			_ => false, [-1, -1]
		}
	}, (false, [-1, -1]))

	return out
}

otherwise is just a compile time alias.


/*! @compiler
  * it instructs compiler to execute this code before parsing code
  * */
@compiler alias //! as otherwise

Palindrome

strA := "may i yam"
strB := "yaay"
strC := "yaya"

isPalindrome(String s): Boolean {
	end := s.length - 1
	mid := Integer.from(end/2) //! from handles 
	i, z := 0, end
	while i < mid && s.get(i) == s.get(z) {
		i += 1
		z -= 1
	}
	return i == mid
}

isPalindrome(strA) // True
isPalindrome(strB) // True
isPalindrome(strC) // False

Command line arguments processing

// go programming book

package main

import (
	"fmt"
	"os"
)

func main() {
	s, sep := "", ""
	for _, arg := range os.Args[1:] {
		s += sep + arg
		sep = " "
	}
	fmt.Println(s)
}

// OR
`strings.Join(os.Args[1:], " ")`
start -> {
	args := process.arguments
	mut s := args.get(0)
	i, e := 1, args.length
	while i < e {
		s += " " + args.get(i))
	}
	process.stdout(s)
}

// OR
List.join(process.arguments, " ")

// OR
List.from(process.arguments).join(" ")

String comparison

equal(String s, String t): Boolean -> {
	return false if s.length != t.length
	return true if s.length == 0
	
	a, s1 := s.get(0), s.slice(1)
	b, t1 := t.get(0), t.slice(1)
	
	return match s1.length {
		0 => a == b
		_ => a == b && equal(s1, t1)
	}
}

// OR
areStringsEqual := s.compare(t) == `equal

Reverse a list

function reverse(alist) { 
	let i = 0, e = alist.length - 1; 
	const out = [];
	while(e >= 0) {
		out[i] = alist[e];
		i += 1;
		e -= 1;
	} 
	return out;
}

// OR
alist.reverse()
reverse(Integer[] ints): Integer[] -> {
	out := []
	i, e := 0, ints.length - 1
	while e >= 0 {
		out[i] = ints[e]
		i += 1
		e -= 1
	}
	return out
}

// OR

reverse(Integer[] ints): Integer[] -> {
	r(alist, out) -> {
		return match ints {
			[] => out
			[first, ...rest] => r(rest, [first] | out)
		}
	}
	return r(ints, [])
}

// OR
ints.reverse()
// OR
List.reverse(ints)

Rotate left

/**
 * from https://github.com/ackret/js.lib/blob/main/src/array/fns/array.js
 * @param {Object} options - The options object.
 * @param options.array {*[]}
 * @param options.rotateBy {number}
 * @returns {*[]}
 */
function rotateLeft({ array, rotateBy }) {
    rotateBy = rotateBy % array.length;
    return [...array.slice(rotateBy), ...array.slice(0, rotateBy)];
}
//! remember, @mutate marks all of the inputs as mutable.
@mutate
rotateLeft(any[] array, Integer rotateBy): any[] -> {
	rotateBy = rotateBy % array.length
	return [...array.slice(rotateBy), ...array.slice(0, rotateBy)]
}

// OR
rotateLeft(any[] array, Integer rotateBy): any[] -> {
	rotateBy = rotateBy % array.length
	return array.slice(rotateBy).concat(array.slice(0, rotateBy))
}

// OR
rotateLeft(any[] array, Integer rotateBy): any[] -> {
	rotateBy = rotateBy % array.length
	left, right := array.splitAt(rotateBy)
	return right.concat(left)
	// OR
	// return right ++ left // if we have overloaded ++ to return a new list from two
}

Spread and Rest are good features in JS. ??

Quick view, Built In data strucutres etc

// Comments

//, /*          comments; part of documentation for public members, functions...
//!, /*!        dev comments; Not part of documentation.
///, /** */     official documentation comments

// Functions

// Lambda: very small scope, enclosed in type, function etc...
x -> x * x
x:number -> x * x

// Standalone functions
add(Number a, Number b): Number ->  a + b
add(Number a, Number b): Number ->  { return a + b }

// Methods 
(Number a) add(Number b):Number -> a + b
(Number a) add(Number b):Number -> { return a + b }

// Function overloading
add(Integer a, Integer b): Integer -> a + b
toString(Integer a): String -> String.from(a)
toString(Number a): String -> String.from(a)
toString(Person p): String -> "$(String.titleCase(p.name)), $(p.age) years old."

// Functions with non-alphanum names
+(String r, String s): String -> String.concat(r, s) // call via +("", "")
++(Number n): Number -> n + 1 // ++(10) will be 11

// Operators (which are basically sugars around functions
// binary is defined by two inputs
@operator +(String a, String b): String -> +(a, b)
// default unary operator is prefix
@operator +(Number a): Number -> Number.abs(a)
// Notice how ++ is not possible without mutation, 
// as that requires code to muck around with memory.

type Name {
	@data {}
	@type {}
	@instance n {}
	@operator {}
}

/**
  * Tests for every function in the type
  * Right next to the member
  */
@test name {
	t1("description"): true -> {
	}
	// OR
	t1("description, _ -> {})
}

// tuples, non-growable iterable group of heterogenous values
(a, b, c) // Tuple.new(a, b, c)

// list, growable iterable group of homogenous values
[a, b, c] // List.new(type Integer, a, b, c)

// map, kv-pair object, no restriction on key or value, could be anything; 
// but homogenous as per type of first key and value
// It is basically an ordered bag for kvp objects
{ k: v, ... }

// Why stop there?

// queues
>> a, b, c >> // simple queue
>> a, b, c << // dequeue

// stack
> a, b, c >

// Tuples, Lists, and Maps can be mapped, reduce, or filtered.
(1, 2, 3).filter(v -> v%2 == 0) // (2)
[1, 2, 3].filter(v -> v%2 == 0) // [2]
{ "one": 1, "two": 2, "tres": 3 }.filter(v -> v.value % 2 == 0) // { "two": 2 }

(1, 2, 3).map(v -> v * v) // (2, 4, 6)

{ "one": 1, "two": 2, "tres": 3 }.map(v -> KVP.new(v.key + v.value, v.value ** v.value))
// { "one1": 2, "two2": 4, "tres3": 27 }


{ "one": 1, "two": 2, "tres": 3 }.reduce(
	init, value, index -> {
		init.sum += value.value
	}, 
	{ "sum": 0 }
)


// Ranges
a..z for end exclusion
a-z for end inclusion
() for a tuple
[] for a list

(0-3) is equivalent to (0, 1, 2, 3)
[0-3] is equivalent to [0, 1, 2, 3]

(0..3) is equivalent to (0, 1, 2)
[0..3] is equivalent to [0, 1, 2]

range(start, end, step) is just a range function

// Loops; no brackets around condition
while condition {}       // The condition must be true for body to be executed
until condition {}      // The condition must be false for the body to be executed

loop n {} // Just run the loop n times; 
			we don't care about index, 
			or how many times we have run the loop so far

no for loop; as I do not feel the need yet.

// control
if condition // mostly used as an expression
unless condition // mostly used as an expression
// match is also an expression, if you put it on the right side of = 
match condition {
	pattern => expression
	...
}
// also, match can work as switch/case or if-else ladder. That's why I don't feel the need for if-else.

// Immutable
// I really want immutable variables by default. Even the arguments.
// Will try to use @mutate and mut to mark functions and variables mutate.
// Will try to use addressof, Address, valueof instead of, say, &, long, *.
// let's hope.

// Observable
// Will introduce @observable at some point for FRP. Not sure if it should be in-built or not

// @cache(N)
// this will cache last N (count) invocations of a function for input -> output combination.

// @pool(N)
// this will create an object pool of N (count) objects at the program boot

// @verifire(N)
// this will call a function N times when you invoke it just once, 
// and see if all of the output are equal
// a simple side-effect test for unstable, side-effect heavy functions
// make of it what you will

// @task
// Instead of coloring functions as async/await, 
// one can mark something as a @task
// and when the output of that task is read, it is automatially synced; 
// instead of marking the reading variable/function as awaiting.
// wut???

// Pipelines
// [firstWord, ...rest] := "hello world" |> .split(" ")
// fizzbuzz
// [1-15] |> 
	n -> match { n % 3 == 0 => "fizz"; _ => "" }, n |> 
	str, n -> match { n % 3 == 0 => str.concat("buzz"); _ => "" }, n |> 
	str, n -> match { str.length > 0 => str; _ => n } |>
	process.stdout

Thoughts on compiler, compilation, etc

  • Multipass compiler
  • Compiler builds dev binary, then run code tests. If pass then only production binary is built.
  • Tests are embedded in binary for a self-test feature, even in production env.
  • @compile lets devs specify and modify things at compile time.
  • Will try to not have any macros
  • Will target golang like quick build, and
  • will try to build all possible production binaries for various architectures via default build
  • docs will be build from code itself, with tests body as example.
  • Compiler will also generate a test suite based on types and their values; this will be a report only.
  • Compiler will come with format, test, perf tools.

Overloading Don't like operator overloading; but function overloading seems nice. Except somehow need to manage type based overloading (C++); and for the same signature: functional style overloading (think haskell, erlang, elixir) which is just bringing out logic based on pattern matching and guards.

Oh that reminds me:

  1. types are not always required; only on boundaries, or when uncertainties are involved.
  2. I like guards but who do those fit in (mostly) statically typed, barely functional language??

Configurations

  • Every code containing directory in a project can have .conf files for configuration;
    • for what, IDK. but an option will be available
  • Tabs and spaces are already sorted by compiler. However I am weak, so will be modifiable via .conf in project root. and many other things.
    • The way it would work is that editors will follow your configuration for reading.
    • Compiler on pre-build will convert to standard; there LSP shenanighans somewhere.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment