Compare commits

...

3 Commits

Author SHA1 Message Date
49faed4fcb Move code snippets to use new component 2024-07-05 16:58:50 -05:00
ebf62bcd5c Remove client side syntax highlighting 2024-07-05 09:40:10 -05:00
631acc8122 Refactor trimming utility 2024-07-05 09:09:49 -05:00
37 changed files with 494 additions and 390 deletions

10
src/components/Code.astro Normal file
View File

@ -0,0 +1,10 @@
---
import { leftTrimDedent } from "./utils";
import { thp_highlighter } from "../lexer/highlighter";
const { thpcode } = Astro.props;
const html_code = thp_highlighter(leftTrimDedent(thpcode).join("\n"));
---
<pre
class="language-thp"><code class="language-thp" set:html={html_code} /><span class="absolute top-2 right-2 inline-block text-sm select-none opacity-75">thp</span></pre>

View File

@ -1,13 +1,11 @@
---
import { trimAndDedent } from "./utils";
import Code from "./Code.astro"
const { thpcode } = Astro.props;
---
<div class="flex h-full py-8 items-center">
<div class="p-4 w-full">
<pre
class="language-thp"
data-language="thp"><code class="language-thp">{trimAndDedent(thpcode).join("\n")}</code></pre>
<Code thpcode={thpcode} />
</div>
</div>

View File

@ -2,7 +2,7 @@
import { lex } from "../lexer/lexer";
import type { Instruction } from "../thp_machine/machine_parser";
import { parse_str } from "../thp_machine/machine_parser";
import { trimAndDedent } from "./utils";
import { leftTrimDedent } from "./utils";
const { code, steps } = Astro.props;
function highlightCode(lines: Array<string>): string {
@ -31,7 +31,7 @@ function highlightCode(lines: Array<string>): string {
return outLines.join("\n");
}
const codeHtml = highlightCode(trimAndDedent(code));
const codeHtml = highlightCode(leftTrimDedent(code));
let instructionSet: Array<Array<Instruction>>;
try {
instructionSet = parse_str(steps);

View File

@ -0,0 +1,133 @@
import { expect, test } from 'vitest'
import { leftTrimDedent } from "./utils"
test("should trim empty string", () => {
const input = ``;
expect(leftTrimDedent(input)).toEqual([""]);
})
test("should work on a single line", () => {
const input = `hello`
expect(leftTrimDedent(input)).toEqual([
"hello"
]);
})
test("should trim a single line", () => {
const input = ` hello`
expect(leftTrimDedent(input)).toEqual([
"hello"
]);
})
test("should trim multiple lines", () => {
const input = ` hello\n world`
expect(leftTrimDedent(input)).toEqual([
"hello",
"world"
]);
})
test("should trim multiple lines without indentation", () => {
const input = `hello\nworld`
expect(leftTrimDedent(input)).toEqual([
"hello",
"world"
]);
})
test("should consume only whitespace", () => {
const input = ` hello\nworld`;
try {
const res = leftTrimDedent(input);
expect(res).not.toEqual([
"hello",
"rld",
]);
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e).toHaveProperty("message", "Invalid indentation while trimming: Expected 2 spaces, got 0");
}
})
test("should preserve deeper indentation", () => {
const input = ` hello\n world`
expect(leftTrimDedent(input)).toEqual([
"hello",
" world",
]);
})
test("should ignore empty lines", () => {
const input = ` hello\n\n world`
expect(leftTrimDedent(input)).toEqual([
"hello",
"",
"world",
]);
})
test("should ignore lines with only whitespace", () => {
const input = ` hello\n \n \n world`
expect(leftTrimDedent(input)).toEqual([
"hello",
"",
" ",
"world",
]);
})
test("should trim multiple without indentation", () => {
const input = `hello\nworld\n!`
expect(leftTrimDedent(input)).toEqual([
"hello",
"world",
"!",
]);
})
test("should ignore empty first line", () => {
const input = `
hello
world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"world",
]);
})
test("should ignore empty first line 2", () => {
const input = `
hello
world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"",
"world",
]);
})
test("should ignore empty last line", () => {
const input = `
hello
world
`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"world",
]);
});

View File

@ -5,15 +5,16 @@
* - Picks the indentation level from the first non-white line
* - Dedents the following lines
*/
export function trimAndDedent(input: string): Array<string> {
export function leftTrimDedent(input: string): Array<string> {
let lines = input.split("\n");
let output: Array<string> = [];
// Remove empty lines at the start
while (lines[0] === "") {
// Ignore first line
if (lines[0] === "" && lines.length > 1) {
lines = lines.slice(1);
}
// Get indentation level
// Get indentation level of the first line
let indentationLevel = 0;
for (const char of lines[0]!) {
if (char === " ") {
@ -23,48 +24,37 @@ export function trimAndDedent(input: string): Array<string> {
}
}
// Enforce indentation, or trim
for (let i = 0; i < lines.length; i += 1) {
// If the line is empty, continue
const characters = lines[i]!.split("");
if (characters.length === 0) {
for (const line of lines) {
// Ignore empty lines
if (line === "") {
output.push("");
continue;
}
// If all characters are whitespace, append just a newline
const nonWhitespace = characters.find((x) => x !== " ");
if (nonWhitespace === undefined) {
lines[i] = "";
continue;
output.push(trimWhitespace(line, indentationLevel));
}
// Enforce indentation
if (characters.length < indentationLevel) {
throw new Error("Invalid indentation while parsing THP code: " + lines[i]);
}
let currentIndentation = 0;
for (const c of characters) {
if (c === " ") { currentIndentation += 1 }
else {break;}
}
if (currentIndentation < indentationLevel) {
throw new Error("Invalid indentation while parsing THP code: " + lines[i]);
if (output.length > 1 && output[output.length - 1] === "") {
output = output.slice(0, -1);
}
lines[i] = characters.slice(4).join("");
}
return output;
}
function trimWhitespace(input: string, count: number): string {
let indentCount = 0;
// Remove empty lines at the end
let endPosition = lines.length - 1;
while (true) {
if (lines[endPosition] === "") {
lines = lines.slice(0, -1);
endPosition -= 1;
for (const char of input) {
if (char === " ") {
indentCount += 1;
} else {
break;
}
}
return lines;
if (indentCount >= count || indentCount == input.length) {
return input.slice(count);
} else {
throw new Error(`Invalid indentation while trimming: Expected ${count} spaces, got ${indentCount}`);
}
}

View File

@ -3,8 +3,7 @@ import Navbar from "../components/Navbar.astro";
import BaseLayout from "./BaseLayout.astro";
import TOC from "../components/TOC.astro";
const {headings} = Astro.props;
const { headings } = Astro.props;
---
<BaseLayout>
@ -41,9 +40,4 @@ const {headings} = Astro.props;
</div>
<div class="h-32"></div>
<script>
import {highlightOnDom} from "./thpHighlighter";
document.addEventListener("DOMContentLoaded", highlightOnDom);
</script>
</BaseLayout>

View File

@ -4,7 +4,13 @@ import BaseLayout from "./BaseLayout.astro";
import TOC from "../components/TOC.astro";
import Sidebar from "../components/Sidebar.astro";
const { frontmatter, headings, posts: _posts, indexSubpath, basePath } = Astro.props;
const {
frontmatter,
headings,
posts: _posts,
indexSubpath,
basePath,
} = Astro.props;
const posts: Record<string, any>[] = _posts;
const indexPage = posts.find((post) => post.file.endsWith(indexSubpath));
@ -30,8 +36,10 @@ if (pagesIndex === undefined) {
function validateEntry(entry: PageEntry, basePath: string) {
if (!entry.children) {
// Attempt to get the page title from frontmatter
const pageData = posts.find((post) =>
post.file.endsWith(entry.path + ".md") || post.file.endsWith(entry.path + ".mdx"),
const pageData = posts.find(
(post) =>
post.file.endsWith(entry.path + ".md") ||
post.file.endsWith(entry.path + ".mdx"),
);
if (pageData === undefined) {
@ -112,7 +120,8 @@ for (const entry of pagesIndex) {
document.addEventListener("DOMContentLoaded", () => {
let current_uri = window.location.pathname;
const sidebar = document.getElementById("sidebar")!.children[0]! as HTMLElement;
const sidebar = document.getElementById("sidebar")!
.children[0]! as HTMLElement;
const links = sidebar.querySelectorAll("a");
for (const link of [...links]) {
if (link.getAttribute("href") === current_uri) {

View File

@ -1,38 +1,5 @@
import { thp_highlighter, CodeJar } from "../lexer/highlighter";
/**
* Highlights all THP code snippets mounted in the DOM with class .language-thp
*
* It assumes that the code is inside: <pre class="language-thp"><code>...
*/
export function highlightOnDom() {
const code_elements = document.querySelectorAll("code.language-thp");
for (const e of [...code_elements]) {
const el = e as HTMLElement;
// Some elements with .language-thp don't want to be processed
if (e.getAttribute("data-disabled") !== null) {
continue;
}
const pre_parent = el.parentElement!;
const new_div = document.createElement("div");
let code = el.innerHTML;
// Replace all &lt; with < and all &gt; with >
code = code.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
el.parentElement!.className = "language-thp";
pre_parent.removeChild(el);
pre_parent.appendChild(new_div);
// Create and append a language name indicator
CodeJar(new_div, thp_highlighter, {
tab: " ",
}).updateCode(code);
}
const pre_elements = document.querySelectorAll("pre");
for (const pre_el of pre_elements) {
const language = pre_el.getAttribute("data-language");

View File

@ -1,9 +1,6 @@
import { lex } from "./lexer";
import {CodeJar as Codejar} from "codejar";
export function thp_highlighter(editor: any) {
let code: string = editor.textContent;
export function thp_highlighter(code: string) {
let tokens = lex(code);
let highlighted_code = "";
@ -12,7 +9,5 @@ export function thp_highlighter(editor: any) {
highlighted_code += `<span class="token ${token.token_type}">${token.v}</span>`;
}
editor.innerHTML = highlighted_code;
return highlighted_code;
}
export const CodeJar = Codejar;

View File

@ -2,6 +2,28 @@
import BaseLayout from "../layouts/BaseLayout.astro";
import Navbar from "../components/Navbar.astro";
import HeroSection from "../components/HeroSection.astro";
import { thp_highlighter } from "../lexer/highlighter";
import { leftTrimDedent } from "../components/utils";
const thpcode = `union Animal {
Dog(String),
Cat(String, Int),
}
val cat_name = Post::get("cat_name") ?? "Michifu"
val my_cat = Animal::Cat(cat_name, 9)
try change_fur_color(cat) else die("Not a cat")
match random_animal()
case Dog(name)
{
print("{name} has 1 live ")
}
case Cat(name, lives)
{
print("{name} has {lives}")
}`;
---
<BaseLayout>
@ -95,7 +117,9 @@ import HeroSection from "../components/HeroSection.astro";
</g>
</svg>
<div class="h-1"></div>
<div id="editor" class="font-mono language-thp"></div>
<div id="editor" class="font-mono language-thp">
<pre set:html={thp_highlighter(leftTrimDedent(thpcode).join("\n"))}></pre>
</div>
</div>
</div>
</div>
@ -194,38 +218,4 @@ import HeroSection from "../components/HeroSection.astro";
<br>
Nullable types must be explicitly checked before using them.
</HeroSection>
<script>
import { thp_highlighter, CodeJar } from "../lexer/highlighter";
let jar = CodeJar(document.getElementById("editor")!, thp_highlighter, {
tab: " ",
});
jar.updateCode(
`union Animal {
Dog(String),
Cat(String, Int),
}
val cat_name = Post::get("cat_name") ?? "Michifu"
val my_cat = Animal::Cat(cat_name, 9)
try change_fur_color(cat) else die("Not a cat")
match random_animal()
case Dog(name)
{
print("{name} has 1 live ")
}
case Cat(name, lives)
{
print("{name} has {lives}")
}`,
);
</script>
<script>
import { highlightOnDom } from "../layouts/thpHighlighter";
document.addEventListener("DOMContentLoaded", highlightOnDom);
</script>
</BaseLayout>

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Comments
---
import Code from "../../../components/Code.astro"
# Comments
@ -12,12 +13,12 @@ THP supports single and multi line comments:
Begin with double slash `//` and continue until the end of the line.
```thp
<Code thpcode={`
// This is a single line comment
print("hello!")
print("the result is {5 + 5}") // This will print 10
```
`} />
## Multi line
@ -26,19 +27,19 @@ These begin with `/*` and end with `*/`. Everything in between is ignored.
Multi line comments can be nested in THP.
```thp
<Code thpcode={`
/*
This is a
multiline comment
*/
```
`} />
```thp
<Code thpcode={`
/*
Multiline comments
can be /* nested */
*/
```
`} />
## Documentation comments

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Datatypes
---
import Code from "../../../components/Code.astro"
# Datatypes
@ -14,7 +15,7 @@ The following are basic datatypes.
Same as php int
```thp
<Code thpcode={`
Int age = 32
// Hexadecimal numbers start with 0x
Int red = 0xff0000
@ -24,10 +25,10 @@ Int permissions = 0o775
Int char_code = 0b01000110
// IMPORTANT!
// Since Octal starts with `0o`, using just a leading 0
// Since Octal starts with \`0o\`, using just a leading 0
// will result in a decimal!
Int not_octal = 032 // This is 32, not 26
```
`} />
## Float
@ -35,10 +36,10 @@ Int not_octal = 032 // This is 32, not 26
Same as php float
```thp
<Code thpcode={`
Float pi = 3.141592
Float light = 2.99e+8
```
`} />
## String
@ -46,15 +47,15 @@ Float light = 2.99e+8
THP strings use **only** double quotes. Single quotes are
used elsewhere.
```thp
<Code thpcode={`
String name = "Rose"
```
`} />
Strings have interpolation with `{}`.
```thp
<Code thpcode={`
print("Hello, {name}") // Hello, Rose
```
`} />
## Bool
@ -62,10 +63,11 @@ print("Hello, {name}") // Hello, Rose
THP booleans are `true` and `false`. They are case sensitive,
**only lowercase.**
```thp
<Code thpcode={`
Bool is_true = true
Bool is_false = false
// This is a compile error
val invalid = TRUE
```
`} />

View File

@ -3,6 +3,7 @@ layout: ../../../layouts/DocsLayout.astro
title: Hello world
---
import InteractiveCode from "../../../components/InteractiveCode.astro";
import Code from "../../../components/Code.astro"
# Hello, world!
@ -16,9 +17,9 @@ detailed later on.
To write a hello world program write the following code in a file:
```thp
<Code thpcode={`
print("Hello, world!")
```
`} />
Then run `thp hello.thp` from your terminal.
@ -34,11 +35,11 @@ echo("B");
echo("C");
```
```thp
<Code thpcode={`
print("A")
print("B")
print("C")
```
`} />
As a consequence of this, there can only be 1 statement per line.

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Operators
---
import Code from "../../../components/Code.astro"
# Operators
@ -10,7 +11,7 @@ Most of the PHP operators are present in THP.
## Numbers
```thp
<Code thpcode={`
var number = 322
number + 1
@ -24,48 +25,48 @@ number -= 1
number *= 1
number /= 1
number %= 2
```
`} />
**There are no prefix/postfix increment**, use `+=` or `-=`.
```thp
<Code thpcode={`
// Use
number += 1
// instead of
number++ // This is a compile error
```
`} />
### Comparison
These operators will not do implicit type conversion. They can
only be used with same datatypes.
```thp
<Code thpcode={`
v1 < v2
v1 <= v2
v1 > v2
v1 >= v2
```
`} />
There is only `==` and `!=`. They are equivalent to `===` and `!==`.
```thp
<Code thpcode={`
v1 == v2
v1 != v2
```
`} />
### Bitwise
TBD
```thp
<Code thpcode={`
number and 1
number or 2
number xor 1
number xand 1
```
`} />
## Strings
@ -73,15 +74,15 @@ TBD.
Strings **do not use `.`** for concatenation. They use `+`.
```thp
<Code thpcode={`
"Hello " + "world."
```
`} />
However, the plus operator `+` does not implicitly convert types.
```thp
<Code thpcode={`
"Hello " + 322 // This is an error
```
`} />
## Boolean
@ -89,10 +90,10 @@ However, the plus operator `+` does not implicitly convert types.
These operators work **only with booleans**, they do not perform
type coercion.
```thp
<Code thpcode={`
c1 && c2
c1 || c2
```
`} />
## Ternary
@ -103,7 +104,7 @@ There is no ternary operator. See [Conditionals](/learn/flow-control/conditional
These are detailed in their section: [Nullable types](/learn/error-handling/null)
```thp
<Code thpcode={`
val person = some_fun()
person?.name
@ -114,7 +115,7 @@ if person?
{
person.name
}
```
`} />

View File

@ -3,6 +3,7 @@ layout: ../../../layouts/DocsLayout.astro
title: Variables
---
import Code from "../../../components/Code.astro"
# Variables
@ -23,27 +24,27 @@ As a regex: `[a-z_][a-zA-Z0-9_]*`
Defined with `val`, followed by a variable name and a value.
```thp
<Code thpcode={`
val surname = "Doe"
val year_of_birth = 1984
```
`} />
### Datatype annotation
Written after the `val` keyword but before the variable name.
```thp
<Code thpcode={`
val String surname = "Doe"
val Int year_of_birth = 1984
```
`} />
When annotating an immutable variable the `val` keyword is optional
```thp
<Code thpcode={`
// Equivalent to the previous code
String surname = "Doe"
Int year_of_birth = 1984
```
`} />
This means that if a variable only has a datatype, it is immutable.
@ -53,28 +54,27 @@ This means that if a variable only has a datatype, it is immutable.
Defined with `var`, followed by a variable name and a value.
```thp
<Code thpcode={`
var name = "John"
var age = 32
```
`} />
### Datatype annotation
Written after the `var` keywords but before the variable name.
```thp
<Code thpcode={`
var String name = "John"
var Int age = 32
```
`} />
When annotating a mutable variable the keyword `var` is still **required**.
```thp
<Code thpcode={`
// Equivalent to the previous code
var String name = "John"
var Int age = 32
```
`} />

View File

@ -2,10 +2,12 @@
layout: ../../../layouts/DocsLayout.astro
title: Anonymous classes
---
import Code from "../../../components/Code.astro"
# Anonymous classes
```thp
<Code thpcode={`
class Logger
{
pub fun log(String msg)
@ -25,9 +27,9 @@ setLogger(class
print(msg)
}
})
```
`} />
```thp
<Code thpcode={`
setLogger(class(Int param1) -> SomeClass(param1), SomeInterface
{
pub fun method()
@ -35,4 +37,4 @@ setLogger(class(Int param1) -> SomeClass(param1), SomeInterface
// code
}
})
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Classes
---
import Code from "../../../components/Code.astro"
# Classes
@ -13,26 +14,26 @@ Syntax and semantics heavily inspired by Kotlin.
To create an instance of a class call it as if it were a function,
without `new`.
```thp
<Code thpcode={`
val animal = Animal()
```
`} />
## Simple class
Classes are declared with the `class` keyword.
```thp
<Code thpcode={`
class Animal
val instance = Animal()
```
`} />
## Properties
Properties are declared with `var`/`val`.
They **must** declare their datatype.
```thp
<Code thpcode={`
class Person
{
// This is an error. Properties must declare their datatype,
@ -42,21 +43,21 @@ class Person
// This is correct
val String name = "Jane Doe"
}
```
`} />
Properties are private by default,
but can be made public with `pub`.
```thp
<Code thpcode={`
class Person
{
// Properties are private by default
val String name = "John Doe"
// To make a property public use `pub`
// To make a property public use \`pub\`
pub var Int age = 30
}
```
`} />
More information about how properties interact with the constructor
is found in the contructor section.
@ -66,7 +67,7 @@ is found in the contructor section.
Methods are declared with `fun`, as regular functions.
```thp
<Code thpcode={`
class Person
{
fun greet()
@ -74,11 +75,11 @@ class Person
print("Hello")
}
}
```
`} />
Methods are private by default, and are made public with `pub`.
```thp
<Code thpcode={`
class Person
{
// This method is private
@ -87,7 +88,7 @@ class Person
print("Hello from private method")
}
// Use `pub` to make a method public
// Use \`pub\` to make a method public
pub fun greet()
{
print("Hello from greet")
@ -97,14 +98,14 @@ class Person
val p = Person()
p.greet() //: Hello from greet
p.private_greet() // Compile time error. Private method.
```
`} />
## This
THP uses the dollar sign `$` as this. It is **required** when
using a class property/method.
```thp
<Code thpcode={`
class Person
{
val String name = "Jane Doe"
@ -120,7 +121,7 @@ class Person
print("Hello, I'm {person_name}")
}
}
```
`} />
## Static members
@ -134,17 +135,17 @@ Static members are detailed in their own page.
PHP only allows a single constructor, and so does THP.
The basic constructor has the syntax of function parameters.
```thp
<Code thpcode={`
// |this is the constructor |
class Animal(String fullname, Int age)
val a1 = Animal("Nal", 4)
```
`} />
The class properties can be declared in the constructor,
using the keywords `pub`, `var`, `val`:
```thp
<Code thpcode={`
class Animal(
// Since we are using val/var, these are promoted to class properties
val String fullname,
@ -160,7 +161,7 @@ class Animal(
val a1 = Animal("Nal", 4)
a1.say() //: My name is Nal and i'm 4 years old
```
`} />
By using this syntax you are declaring properties and assigning them
at the same time.
@ -173,14 +174,14 @@ The contructor parameters can also have default values.
The constructor is public by default. It can be made private/protected
like this:
```thp
<Code thpcode={`
class Animal
private constructor(
val String fullname,
var Int age,
)
{...}
```
`} />
### Derived properties
@ -188,19 +189,19 @@ private constructor(
You can declare properties whose values depend on values
on the constructor.
```thp
<Code thpcode={`
class Animal(
val String fullname,
)
{
// A property whose value depends on `fullname`
// A property whose value depends on \`fullname\`
// This is executed after the contructor
pub val Int name_length = $fullname.length
}
val a2 = Animal("Doa")
print(a2.name_length) //: 3
```
`} />
### Init block
@ -208,7 +209,7 @@ print(a2.name_length) //: 3
If you need to additional logic in the constructor you can
use a `init` block.
```thp
<Code thpcode={`
class Animal(
val String fullname,
)
@ -220,13 +221,13 @@ class Animal(
}
val a3 = Animal("Lola") //: Lola in construction
```
`} />
## Inheritance
```thp
<Code thpcode={`
// Base class
class Animal(var String name)
{
@ -241,13 +242,13 @@ class Cat(String name, Int lives)
extends Animal(name)
Cat("Michi", 9).say_name()
```
`} />
## Mutable methods
By default methods cannot mutate the state of the object.
```thp
<Code thpcode={`
class Animal(var String name)
{
pub fun set_name(String new_name)
@ -255,12 +256,12 @@ class Animal(var String name)
$name = new_name // Error: Cannot mutate $
}
}
```
`} />
To do so the method must be annotated. The caller must also
declare a mutable variable.
```thp
<Code thpcode={`
class Animal(var String name)
{
pub mut fun set_name(String new_name)
@ -271,13 +272,13 @@ class Animal(var String name)
var michi = Animal("Michifu")
michi.set_name("Garfield")
```
`} />
## Class constructor that may return an error
Working theory:
```thp
<Code thpcode={`
class Fish(
val String name,
var Int lives = 9,
@ -285,9 +286,9 @@ class Fish(
extends Animal(name)
throws Error
val fish_result = Fish("bubble") // fish_result will be a `Result[Fish,Error]`
val fish_result = Fish("bubble") // fish_result will be a \`Result[Fish,Error]\`
val fish = try fish_result
```
`} />

View File

@ -2,12 +2,13 @@
layout: ../../../layouts/DocsLayout.astro
title: Interfaces
---
import Code from "../../../components/Code.astro"
# Interfaces
```thp
<Code thpcode={`
interface Serializable
{
// Methods are always public in interfaces
@ -24,7 +25,7 @@ class Cat -> Serializable
}
}
```
`} />
No interface inheritance.

View File

@ -2,12 +2,13 @@
layout: ../../../layouts/DocsLayout.astro
title: Magic methods
---
import Code from "../../../components/Code.astro"
# Magic methods
Don't get special treatment.
```thp
<Code thpcode={`
class Cat
{
@ -17,13 +18,13 @@ class Cat
}
}
```
`} />
```thp
<Code thpcode={`
val option = Some("GAAA")
val Some(value) = option
val colors = Array("red", "green", "blue")
val Array()
```
`} />

View File

@ -2,13 +2,14 @@
layout: ../../../layouts/DocsLayout.astro
title: Static
---
import Code from "../../../components/Code.astro"
# Static in classes
## Class constants
```thp
<Code thpcode={`
class Cat
{
// Stateful code
@ -20,7 +21,7 @@ static Cat
}
print(Cat::CONSTANT)
```
`} />
## Static methods
@ -28,7 +29,7 @@ print(Cat::CONSTANT)
aka. plain, old functions
```thp
<Code thpcode={`
static Cat
{
fun static_method() -> Int
@ -38,7 +39,7 @@ static Cat
}
Cat::static_method()
```
`} />
## Static properties
@ -46,7 +47,7 @@ Cat::static_method()
aka. global variables
```thp
<Code thpcode={`
static Cat
{
pub var access_count = 0
@ -55,7 +56,7 @@ static Cat
print(Cat::access_count) // 0
Cat::access_count += 1
print(Cat::access_count) // 1
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Arrays
---
import Code from "../../../components/Code.astro"
# Arrays
@ -9,7 +10,7 @@ Use square brackets as usual.
## Usage
```thp
<Code thpcode={`
val fruits = ["apple", "banana", "cherry"]
val apple = fruits[0]
@ -21,16 +22,16 @@ var numbers = [0, 1, 2, 3]
numbers[3] = 5
print(numbers[3]) // 5
```
`} />
## Type signature
```thp
<Code thpcode={`
Array[String]
Array[Int]
```
`} />
The Array signature __requires__ the word `Array`.
There is no `Int[]` or `[Int]` signature, since that would cause

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Enums
---
import Code from "../../../components/Code.astro"
# Enums
@ -15,7 +16,7 @@ THP enums are a 1 to 1 map of PHP enums, with a slightly different syntax.
Enums don't have a scalar value by default.
```thp
<Code thpcode={`
enum Suit
{
Hearts,
@ -25,14 +26,14 @@ enum Suit
}
val suit = Suit::Hearts
```
`} />
## Backed enums
Backed enums can have a scalar for each case. The scalar values can only be
`String` or `Int`.
```thp
<Code thpcode={`
enum Suit(String)
{
Hearts = "H",
@ -40,7 +41,7 @@ enum Suit(String)
Clubs = "C",
Spades = "S",
}
```
`} />
All cases must explicitly define their value, there is no automatic generation.

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Maps
---
import Code from "../../../components/Code.astro"
# Maps
@ -14,7 +15,7 @@ There can also be anonymous maps, which may contain any key.
## Named Maps
```thp
<Code thpcode={`
// Here we define a map, called Person
map Person {
String name,
@ -36,21 +37,21 @@ var Person mary_jane = .{
surname: "Jane",
age: 27,
}
```
`} />
To access the fields of a map we use square braces `[]`.
```thp
<Code thpcode={`
mary_jane["age"] += 1
print(mary_jane["name"]) // Mary
```
`} />
Or dot access `.` if the field's name is a valid identifier.
```thp
<Code thpcode={`
mary_jane.age += 1
print(mary_jane.name)
```
`} />
## Anonymous maps
@ -58,23 +59,23 @@ print(mary_jane.name)
An anonymous map allows us to store and retrieve any key of any datatype.
They are declared as `Map`.
```thp
<Code thpcode={`
val car = Map {
brand: "Toyota",
model: "Corolla",
year: 2012,
}
```
`} />
Anonymous maps can also can have their type omitted.
```thp
<Code thpcode={`
var car = .{
brand: "Toyota",
model: "Corolla",
year: 2012,
}
```
`} />
If the compiler encounters a map without a type (that is, `.{}`)
and doesn't expect a specific type, it will assume it is an
@ -82,30 +83,30 @@ anonymous map.
We can freely assign fields to an anonymous map:
```thp
<Code thpcode={`
// Modify an existing field
car["year"] = 2015
// Create a new field
car["status"] = "used"
```
`} />
However, if we try to access a field of an anonymous map we'll get
a nullable type, and we must annotate it.
```thp
<Code thpcode={`
// This is ok, we are declaring what datatype we expect
String? car_status = car["status"]
// This won't work, the compiler doesn't know what datatype to use
var car_status = car["status"]
```
`} />
Instead, we can use the `get` function of the map, which expects a
datatype and returns that type as nullable
```thp
<Code thpcode={`
val car_status = car.get[String]("status")
```
`} />
Both ways to get a value will check that the key exists in the map,
and that it has the correct datatype. If either the key doesn't exist
@ -113,10 +114,10 @@ or it has a different datatype, it will return `null`.
We can also use dynamic keys, following the same rules:
```thp
<Code thpcode={`
val generated_value = "key"
String? v = map[generated_value]
// or
val v = map[String](generated_value)
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Tuples
---
import Code from "../../../components/Code.astro"
# Tuples
@ -9,18 +10,19 @@ Uses `#()` just to avoid confusion with function calls and grouping (`()`).
## Definition
```thp
<Code thpcode={`
val person = #("John", "Doe", 32)
val #(name, surname, age) = person
```
`} />
## Signature
```thp
<Code thpcode={`
#(String, String, Int)
```
`} />

View File

@ -2,12 +2,13 @@
layout: ../../../layouts/DocsLayout.astro
title: Tagged unions
---
import Code from "../../../components/Code.astro"
# Tagged unions
Tagged unions can hold a value from a fixed selection of types.
```thp
<Code thpcode={`
union Shape
{
Dot,
@ -18,11 +19,11 @@ union Shape
val dot = Shape::Dot
val square1 = Shape::Square(10)
val rectangle1 = Shape::Rectangle(5, 15)
```
`} />
## Pattern matching
```thp
<Code thpcode={`
match shape_1
case ::Square(side)
{
@ -32,7 +33,7 @@ case ::Rectangle(length, height)
{
print("Area of the rectangle: {length * height}")
}
```
`} />
## Internal representation

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Nullable types
---
import Code from "../../../components/Code.astro"
# Nullable types
@ -12,18 +13,18 @@ by the question mark `?` character.
For instance, a POST request may have a `username` parameter,
or it may not. This can be represented with an `String?`.
```thp
<Code thpcode={`
String? new_username = POST::get("username")
```
`} />
When we have a `Type?` we cannot use it directly. We must first
check if the value is null, and then use it.
```thp
<Code thpcode={`
if new_username != null {
// Here `new_username` is automatically casted to String
// Here \`new_username\` is automatically casted to String
}
```
`} />
We must check explicitly that the value is not null. Doing
`if new_username {}` alone is not allowed.
@ -32,30 +33,30 @@ We must check explicitly that the value is not null. Doing
To create a nullable type we must explicitly annotate the type.
```thp
<Code thpcode={`
val favorite_color = null // Error, we must define the type
String? favorite_color = null // Ok
```
`} />
Other examples:
```thp
<Code thpcode={`
fun get_first(Array[String?] values) -> String? {}
val result = get_first([])
```
`} />
## Optional chaining
If you have a `Type?` and you wish to access a field of `Type` if it exists,
you can use the optional chaining operator.
```thp
<Code thpcode={`
Person? person = ...
val name = person?.name
```
`} />
- If `person` is null, `person?.name` will return `null`
- If `person` is not null, `person?.name` will return `name`
@ -65,12 +66,12 @@ val name = person?.name
The Elvis operator `??` is used to give a default value in case a `null` is found.
```thp
<Code thpcode={`
// This is a function that may return a Int
fun get_score() -> Int? {...}
val test_score = get_score() ?? 0
```
`} />
For the above code:
@ -79,9 +80,9 @@ For the above code:
You can use the Elvis operator to return early
```thp
<Code thpcode={`
val username = get_username() ?? return
```
`} />

View File

@ -3,6 +3,7 @@ layout: ../../../layouts/DocsLayout.astro
title: Try/Exceptions
---
import InteractiveCode from "../../../components/InteractiveCode.astro";
import Code from "../../../components/Code.astro"
# Try/exceptions
@ -17,7 +18,7 @@ is used.
For example, a function that returned a `DivisionByZero`
may be written like this:
```thp
<Code thpcode={`
fun invert(Int number) -> Int!DivisionByZero
{
if number == 0
@ -27,7 +28,7 @@ fun invert(Int number) -> Int!DivisionByZero
return 1 / number
}
```
`} />
In the previous segment, `Int!DivisionByZero` denotates
that the function may return either an `Int` or an `DivisionByZero`.
@ -44,12 +45,12 @@ TODO: fix?
If there are multiple error types that the function can return,
you can use the `|` operator:
```thp
<Code thpcode={`
type Exceptions = Exception1 | Exception2 | Exception3
fun sample() -> Int!Exceptions
{ /* ... */}
```
`} />
@ -132,14 +133,14 @@ otherwise will assign the success value and continue.
Try/return will run a function and assign its value if `Ok` is found.
Otherwise, it will return a new value specified by the programmer.
```thp
<Code thpcode={`
fun run() -> Int
{
val result = try dangerous() return 0
// ...
}
```
`} />
In the previous example:
@ -226,27 +227,27 @@ Either way, the function will continue executing.
Try/catch allows the error to be manually used & handled.
```thp
<Code thpcode={`
fun run()
{
val result = try dangerous()
catch Exception e
{
// This is run if `dangerous()` throws.
// `e` is the thrown error
// This is run if \`dangerous()\` throws.
// \`e\` is the thrown error
// Handle the error
// ...
// Return a new value to be assigned to `result`
// Return a new value to be assigned to \`result\`
0
}
}
```
`} />
A try/catch may have many `catch` clauses:
```thp
<Code thpcode={`
try dangerous()
catch Exception1 e
{...}
@ -254,6 +255,6 @@ catch Exception2 e
{...}
catch Exception3 e
{...}
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Blocks
---
import Code from "../../../components/Code.astro"
# Blocks
@ -11,13 +12,13 @@ Blocks can be assigned to variables, in which case
they are executed and their last expression is
returned.
```thp
<Code thpcode={`
val result = {
val temp = 161
temp * 2 // This will be assigned to `result`
temp * 2 // This will be assigned to \`result\`
}
print(result) // 322
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Conditionals
---
import Code from "../../../components/Code.astro"
# Conditionals
@ -12,7 +13,7 @@ title: Conditionals
- There's no ternary operator
```thp
<Code thpcode={`
if condition
{
// code
@ -28,25 +29,25 @@ else
val result = if condition { value1 } else { value2 }
```
`} />
## Check for datatypes
```thp
<Code thpcode={`
if variable is Datatype
{
// code using variable
}
```
`} />
## If variable is of enum
TBD
```thp
<Code thpcode={`
val user_id = POST::get("user_id")
if Some(user_id) = user_id
@ -58,7 +59,7 @@ if Some(user_id) = user_id && user_id > 0
{
print("user_id is greater than 0: {user_id}")
}
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Loops
---
import Code from "../../../components/Code.astro"
# Loops
@ -11,16 +12,16 @@ This is simmilar to PHP's `foreach`. There is no equivalent to PHP's `for`.
Braces are required.
```thp
<Code thpcode={`
val numbers = [0, 1, 2, 3]
for #(index, number) in numbers
{
print(number)
}
```
`} />
```thp
<Code thpcode={`
val dict = .{
apple: 10,
banana: 7,
@ -31,13 +32,13 @@ for #(key, value) in dict
{
print("{key} => {value}")
}
```
`} />
## While loop
```thp
<Code thpcode={`
val colors = ["red", "green", "blue"]
var index = 0
@ -46,14 +47,14 @@ while index < colors.size()
print("{colors[index]}")
index += 1
}
```
`} />
## Infinite loop
Instead of doing `while true {}` use `loop`.
```thp
<Code thpcode={`
loop
{
print("looping")
@ -63,14 +64,14 @@ loop
break
}
}
```
`} />
## Labelled loops
You can give labels to loops, allowing you to `break` and `continue` in nested loops.
```thp
<Code thpcode={`
:top for i in values_1
{
for j in values_2
@ -79,12 +80,6 @@ You can give labels to loops, allowing you to `break` and `continue` in nested l
break :top
}
}
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Match
---
import Code from "../../../components/Code.astro"
# Match
@ -9,7 +10,7 @@ title: Match
Braces are **required**.
```thp
<Code thpcode={`
val user_id = POST::get("user_id")
@ -47,5 +48,5 @@ else
{
print("hello dear")
}
```
`} />

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Declaration
---
import Code from "../../../components/Code.astro"
# Declaration
@ -10,42 +11,42 @@ Function names **must** begin with a lowercase letter.
## No parameters, no return
```thp
<Code thpcode={`
fun say_hello()
{
print("Hello")
}
say_hello()
```
`} />
## With return type
```thp
<Code thpcode={`
fun get_random_number() -> Int
{
Random::get(0, 35_222)
}
val number = get_random_number()
```
`} />
## With parameters and return type
```thp
<Code thpcode={`
fun get_secure_random_number(Int min, Int max) -> Int
{
Random::get_secure(min, max)
}
val number = get_secure_random_number(0, 65535)
```
`} />
## Generic types
```thp
<Code thpcode={`
fun get_first_item[T](Array[T] array) -> T
{
array[0]
@ -55,23 +56,23 @@ val first = get_first_item[Int](numbers)
// The type annotation is optional if the compiler can infer the type
val first = get_first_item(numbers)
```
`} />
## Signature
```thp
<Code thpcode={`
() -> ()
() -> Int
(Int, Int) -> Int
[T](Array[T]) -> T
```
`} />
## Named arguments
```thp
<Code thpcode={`
fun html_special_chars(
String input,
Int? flags,
@ -83,13 +84,13 @@ fun html_special_chars(
}
html_special_chars(input, double_encoding: false)
```
`} />
TBD: If & how named arguments affect the order of the parameters
## Named arguments with different names
```thp
<Code thpcode={`
fun greet(
String name,
String from: city,
@ -99,7 +100,7 @@ fun greet(
}
greet("John", from: "LA")
```
`} />

View File

@ -2,23 +2,24 @@
layout: ../../../layouts/DocsLayout.astro
title: Higher Order Functions
---
import Code from "../../../components/Code.astro"
# Higher Order functions
## Function as parameter
```thp
<Code thpcode={`
fun map[A, B](Array[A] input, (A) -> B function) -> Array[B]
{
// implementation
}
```
`} />
## Function as return
```thp
<Code thpcode={`
fun generate_generator() -> () -> Int
{
// code...
@ -30,7 +31,7 @@ fun generate_generator() -> () -> Int
val generator = generate_generator() // A function
val value = generate_generator()() // An Int
```
`} />

View File

@ -2,13 +2,14 @@
layout: ../../../layouts/DocsLayout.astro
title: Lambdas
---
import Code from "../../../components/Code.astro"
# Lambdas / Anonymous functions
## Anonymous function
```thp
<Code thpcode={`
fun(Int x, Int y) -> Int {
x + y
}
@ -17,7 +18,7 @@ fun(Int x, Int y) -> Int {
numbers.map(fun(x) {
x * 2
})
```
`} />
@ -26,7 +27,7 @@ numbers.map(fun(x) {
By default closures **always** capture variables as **references**.
```thp
<Code thpcode={`
var x = 20
val f = fun() {
@ -37,18 +38,18 @@ f() // 20
x = 30
f() // 30
```
`} />
You can force a closure to capture variables by value.
```thp
<Code thpcode={`
fun(parameters) clone(variables) {
// code
}
```
`} />
```thp
<Code thpcode={`
var x = 20
val f = fun() clone(x) {
@ -59,7 +60,7 @@ f() // 20
x = 30
f() // 20
```
`} />
## Lambdas
@ -70,7 +71,7 @@ Inside the lambda you can access the parameters as `$1`, `$2`, etc.
Finally, lambdas be written outside of the parenthesis of function calls.
```thp
<Code thpcode={`
numbers.map() #{
$1 * 2
}
@ -80,7 +81,7 @@ numbers.map() #{
numbers.map(fun(param1) {
$1 * 2
})
```
`} />

View File

@ -2,39 +2,40 @@
layout: ../../../layouts/DocsLayout.astro
title: Function parameters
---
import Code from "../../../components/Code.astro"
# Function parameters
## Immutable reference
```thp
<Code thpcode={`
fun add_25(Array[Int] numbers) {
numbers.push(25) // Error: `numbers` is immutable
numbers.push(25) // Error: \`numbers\` is immutable
}
```
`} />
When using a regular type as a parameter, only it's immutable
properties can be used
```thp
<Code thpcode={`
fun count(Array[Int] numbers) -> Int {
val items_count = numbers.size() // Ok, `size` is pure
val items_count = numbers.size() // Ok, \`size\` is pure
items_count
}
```
`} />
To use immutable properties you must use a mutable reference.
## Mutable reference
```thp
<Code thpcode={`
fun push_25(mut Array[Int] numbers) {
numbers.push(25) // Ok, will also mutate the original array
}
```
`} />
Placing a `mut` before the type makes the parameter a mutable
reference. Mutable methods can be used, and the original
@ -42,37 +43,34 @@ data **can** be mutated.
The caller *must* also use `mut`.
```thp
<Code thpcode={`
val numbers = Array(0, 1, 2, 3)
push_25(mut numbers) // Pass `numbers` as reference.
push_25(mut numbers) // Pass \`numbers\` as reference.
print(numbers(4)) // `Some(25)`
```
print(numbers(4)) // \`Some(25)\`
`} />
## Clone
```thp
<Code thpcode={`
fun add_25(clone Array[Int] numbers) {
numbers.push(25) // Ok, the original array is unaffected
}
```
`} />
Using the `clone` keyword before the type creates a mutable copy
of the parameter (CoW). The original data will **not** be mutated.
```thp
<Code thpcode={`
val numbers = Array(1, 2, 3, 4)
add_25(clone numbers) // Pass `numbers` as clone.
add_25(clone numbers) // Pass \`numbers\` as clone.
print(numbers(4)) // None
```
`} />

View File

@ -54,6 +54,7 @@ pagesLayout:
- path: intro
---
import InteractiveCode from "../../components/InteractiveCode.astro";
import Code from "../../components/Code.astro"
# Welcome
@ -144,11 +145,11 @@ $has_key = str_contains($haystack, 'needle');
print("has key? " . $has_key);
```
```thp
<Code thpcode={`
// THP
val has_key = haystack.contains("needle")
print("has key? " + has_key)
```
`} />
- Explicit variable declaration
- No `$` for variable names (and thus no `$$variable`, use a map instead)
@ -170,14 +171,14 @@ $obj = [
]
```
```thp
<Code thpcode={`
// THP
val obj = .{
names: #("Toni", "Stark"), // Tuple
age: 33,
numbers: [32, 64, 128]
}
```
`} />
- Tuples, Arrays, Sets, Maps are clearly different
- JSON-like object syntax
@ -192,11 +193,11 @@ $cat->meow();
```
```thp
<Code thpcode={`
// THP
val cat = Cat("Michifu", 7)
cat.meow();
```
`} />
- Instantiate classes without `new`
- Use dot `.` instead of arrow `->` syntax
@ -211,10 +212,10 @@ use \Some\Deeply\Nested\Interface
```
```thp
<Code thpcode={`
// THP
use Some::Deeply::Nested::{Class, Interface}
```
`} />
- Different module syntax
- Explicit module declaration
@ -237,7 +238,7 @@ Where possible THP will compile to available PHP functions/classes/methods/etc.
For example:
```thp
<Code thpcode={`
// This expression
val greeting =
match get_person()
@ -253,7 +254,7 @@ val greeting =
{
"Nobody is here"
}
```
`} />
```php
// Would compile to:

View File

@ -2,6 +2,7 @@
layout: ../../../layouts/DocsLayout.astro
title: Introduction
---
import Code from "../../../components/Code.astro"
# THP templating
@ -60,18 +61,19 @@ and compose them.
The following would be the equivalent in THP:
```thp
<Code thpcode={`
fun Button(String name) -> Html {
<button class="some tailwind classes">
Hello {name}!
</button>
}
```
`} />
It is very similar to React. The HTML is inside the THP code, not the other
way around, so you can have arbitrary logic in the component.
```thp
<Code thpcode={`
fun User(String name) {
// Get info from the database
val user = try Model::get_user(name)
@ -96,9 +98,6 @@ fun TransactionItem(Transaction t) {
{t.date} - {t.name} ({t.price})
</li>
}
```
`} />