Compare commits
3 Commits
5208496124
...
49faed4fcb
Author | SHA1 | Date | |
---|---|---|---|
49faed4fcb | |||
ebf62bcd5c | |||
631acc8122 |
10
src/components/Code.astro
Normal file
10
src/components/Code.astro
Normal 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>
|
@ -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>
|
||||
|
@ -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);
|
||||
|
133
src/components/utils.test.ts
Normal file
133
src/components/utils.test.ts
Normal 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",
|
||||
]);
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
}
|
||||
|
||||
lines[i] = characters.slice(4).join("");
|
||||
output.push(trimWhitespace(line, indentationLevel));
|
||||
}
|
||||
|
||||
if (output.length > 1 && output[output.length - 1] === "") {
|
||||
output = output.slice(0, -1);
|
||||
}
|
||||
|
||||
// Remove empty lines at the end
|
||||
let endPosition = lines.length - 1;
|
||||
while (true) {
|
||||
if (lines[endPosition] === "") {
|
||||
lines = lines.slice(0, -1);
|
||||
endPosition -= 1;
|
||||
return output;
|
||||
}
|
||||
|
||||
function trimWhitespace(input: string, count: number): string {
|
||||
let indentCount = 0;
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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 < with < and all > with >
|
||||
code = code.replace(/</g, "<").replace(/>/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");
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
```
|
||||
`} />
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
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
|
||||
}
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
```
|
||||
`} />
|
@ -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
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
```
|
||||
`} />
|
@ -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
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
@ -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.
|
||||
|
@ -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)
|
||||
```
|
||||
`} />
|
@ -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)
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
{...}
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
`} />
|
||||
|
@ -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}")
|
||||
}
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
`} />
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
```
|
||||
`} />
|
||||
|
@ -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")
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
```
|
||||
`} />
|
||||
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
||||
`} />
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
`} />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user