Compare commits

...

7 Commits

Author SHA1 Message Date
Shadowfacts a39a938d08 Add Part 12: Typed Variables 2022-05-25 17:32:31 -04:00
Shadowfacts 7d8754ad82 Add remote interaction 2022-05-16 22:25:13 -04:00
Shadowfacts b5f101a4e7 Fix 500 page title 2022-05-15 23:49:44 -04:00
Shadowfacts ff0c7a314d Add goatcounter 2022-05-15 23:16:15 -04:00
Shadowfacts af4c212796 Remove unneeded zoom-disabling viewport meta 2022-05-15 23:15:53 -04:00
Shadowfacts 685b75cfc9 Add rust tag 2022-05-15 21:27:55 -04:00
Shadowfacts d9b98519cd Fix typo 2022-05-15 21:26:11 -04:00
13 changed files with 366 additions and 9 deletions

View File

@ -4,6 +4,7 @@ import comments from "./comments";
import federate from "./federate";
import followers from "./followers";
import inbox from "./inbox";
import interact from "./interact";
import nodeinfo from "./nodeinfo";
import webfinger from "./webfinger";
@ -14,6 +15,7 @@ export = {
federate,
followers,
inbox,
interact,
nodeinfo,
webfinger
};

View File

@ -0,0 +1,18 @@
import {Router} from "express";
import {queryWebfinger} from "./webfinger";
const domain = process.env.DOMAIN;
export default async function interact(router: Router) {
router.post("/interact", async (req, res) => {
const permalink = req.body.permalink;
const acct = req.body.remote_follow.acct;
const webfingerResult = await queryWebfinger(acct);
const link = webfingerResult.links.find((l) => l.rel === "http://ostatus.org/schema/1.0/subscribe");
if (link && 'template' in link) {
res.redirect(link.template.replace("{uri}", `https://${domain}${permalink}`));
} else {
res.status(400).send("Unable to find remote subscribe URL");
}
});
}

View File

@ -1,9 +1,21 @@
import express, { Router } from "express";
import fetch from "node-fetch";
const domain = process.env.DOMAIN;
export default function webfinger(router: Router) {
router.get("/.well-known/webfinger", (req, res) => {
res.json({
subject: `acct:block@${domain}`,
aliases: [`https://${domain}/ap/actor`],
links: [
{
rel: "self",
type: "application/activity+json",
href: `https://${domain}/ap/actor`,
}
]
} as WebfingerResult);
res.json({
"subject": `acct:blog@${domain}`,
"links": [
@ -16,4 +28,23 @@ export default function webfinger(router: Router) {
});
res.end();
});
}
}
export async function queryWebfinger(acct: string): Promise<WebfingerResult> {
if (acct.startsWith("@")) {
acct = acct.substring(1);
}
const parts = acct.split("@");
if (parts.length !== 2) {
throw "Invalid account";
}
const response = await fetch(`https://${parts[1]}/.well-known/webfinger?resource=${acct}`);
const json = await response.json();
return json as WebfingerResult;
}
export interface WebfingerResult {
subject: string;
aliases: string[];
links: Array<{rel: string, type: string, href: string} | {rel: "https://ostatus.org/schema/1.0/subscribe", template: string}>;
}

View File

@ -48,6 +48,7 @@ function watch() {
const app = express();
app.use(morgan("dev"));
app.use(bodyParser.json({ type: "application/activity+json" }));
app.use(bodyParser.urlencoded());
const connection = await createConnection({
"type": "postgres",
@ -82,6 +83,8 @@ function watch() {
await activitypub.articles.setup(posts);
await activitypub.interact(app);
const apRouter = Router();
apRouter.use(validateHttpSig);
await activitypub.actor(apRouter);

66
package-lock.json generated
View File

@ -31,6 +31,7 @@
"markdown-it": "^8.4.2",
"markdown-it-footnote": "^3.0.2",
"morgan": "^1.9.1",
"node-fetch": "^2.6.7",
"pg": "^8.5.1",
"reflect-metadata": "^0.1.13",
"request": "^2.88.0",
@ -1746,6 +1747,25 @@
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -2511,6 +2531,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"node_modules/ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
@ -2877,6 +2902,20 @@
"extsprintf": "^1.2.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -4305,6 +4344,14 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -4911,6 +4958,11 @@
}
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
@ -5169,6 +5221,20 @@
"extsprintf": "^1.2.0"
}
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@ -29,6 +29,7 @@
"markdown-it": "^8.4.2",
"markdown-it-footnote": "^3.0.2",
"morgan": "^1.9.1",
"node-fetch": "^2.6.7",
"pg": "^8.5.1",
"reflect-metadata": "^0.1.13",
"request": "^2.88.0",

View File

@ -1,5 +1,5 @@
```
metadata.title = "Not Found"
metadata.title = "Server Error"
metadata.layout = "default.html.ejs"
```

View File

@ -354,7 +354,47 @@ article {
}
#comments-info {
margin-top: 0;
margin: 0;
}
#remote-interact {
display: flex;
flex-direction: row;
align-items: baseline;
input {
margin-left: 4px;
}
input[type=text] {
flex-grow: 1;
padding: 0 4px;
background-color: var(--content-background-color);
border: 1px solid var(--accent-color);
font-size: 1rem;
line-height: 2rem;
color: var(--content-text-color);
}
input[type=submit] {
background-color: var(--ui-background-color);
border: 1px solid var(--accent-color);
color: var(--accent-color);
line-height: 2rem;
padding: 0 1rem;
text-decoration: none;
font-weight: bold;
text-transform: uppercase;
-webkit-transition: 0.3s ease-out;
transition: 0.3s ease-out;
&:hover {
background-color: var(--accent-color);
color: var(--ui-background-color);
cursor: pointer;
}
}
}
#comments-js-warning {

View File

@ -23,9 +23,20 @@ metadata.layout = "default.html.ejs"
<h2 id="comments-container-title">Comments</h2>
</summary>
<p id="comments-info">
Comments powered by ActivityPub. To respond to this post or to another comment, copy its URL into the search interface of your client for Mastodon, Pleroma, or other compatible software.
Comments powered by ActivityPub. To respond to this post enter your username and instance below, or copy its URL into the search interface of your client for Mastodon, Pleroma, or other compatible software.
<a href="/2019/reincarnation/#activity-pub">Learn more</a>.
</p>
<form action="/interact" method="POST" id="remote-interact">
<span>Reply from your instance:</span>
<% if (metadata.useOldPermalinkForComments) { %>
<input type="hidden" name="permalink" value="<%= metadata.oldPermalink %>">
<% } else { %>
<input type="hidden" name="permalink" value="<%= metadata.permalink %>">
<% } %>
<!-- name needs to be exactly that to get the browser to use same completions as mastodon -->
<input type="text" placeholder="Enter your user@domain" required id="acct" name="remote_follow[acct]">
<input type="submit" value="Interact">
</form>
<noscript>
<p id="comments-js-warning">
JavaScript is required to display comments.

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= metadata.title %></title>
@ -116,5 +116,7 @@
</noscript>
</footer>
<script data-goatcounter="https://shadowfacts.goatcounter.com/count" async src="//gc.zgo.at/count.v3.js" crossorigin="anonymous" integrity="sha384-QGgNMMRFTi8ul5kHJ+vXysPe8gySvSA/Y3rpXZiRLzKPIw8CWY+a3ObKmQsyDr+a"></script>
</body>
</html>

View File

@ -12,7 +12,7 @@ Armed with the ID3 decoder from my [last post](/2020/parsing-id3-tags/), we can
First, a overview of the anatomy of an MP3 file. As we went through in the last post, an MP3 file may optionally start with an ID3 tag containing metadata about the track. After that comes the meat of the file: a sequence of MP3 frames. Each frame contains a specific amount of encoded audio data (the actual amount is governed by a number of properties encoded in the frame header). The total duration of the file is simply the sum of the durations of the individual frames. Each MP3 frame starts with a marker sequence of 11 1-bits followed by three bytes of flags describing the contents of the frame and then the audio data itself.
Based on this information, many posts on various internet formus suggest inspecting the first frame to find its bitrate, and then dividing the bit size of the entire file by the bitrate to get the total duration. There are a few problems with this though. The biggest is that whole MP3 files can generally be divided into two groups: constant bitrate (CBR) files and variable bitrate (VBR) ones. Bitrate refers to the number of bits used to represent audio data for a certain time interval. As the name suggests, files encoded with a constant bitrate use the same number of bits per second to represent audio data throughout the entire file. For the naive length estimation method, this is okay (though not perfect, because it doesn't account for the frame headers and any potential unused space in between frames). In variable bitrate MP3s though, each frame can have a different bitrate, which allows the encoder to work more space-efficiently (because portions of the audio that are less complex can be encoded at a lower bitrate). Because of this, the naive estimation doesn't work at all (unless, by coincidence, the bitrate of the first frame happens to be close to the average bitrate for the entire file). In order to accurately get the duration for a VBR file, we need to go through every single frame in the file and sum their individual durations. So that's what we're gonna do.
Based on this information, many posts on various internet forums suggest inspecting the first frame to find its bitrate, and then dividing the bit size of the entire file by the bitrate to get the total duration. There are a few problems with this though. The biggest is that whole MP3 files can generally be divided into two groups: constant bitrate (CBR) files and variable bitrate (VBR) ones. Bitrate refers to the number of bits used to represent audio data for a certain time interval. As the name suggests, files encoded with a constant bitrate use the same number of bits per second to represent audio data throughout the entire file. For the naive length estimation method, this is okay (though not perfect, because it doesn't account for the frame headers and any potential unused space in between frames). In variable bitrate MP3s though, each frame can have a different bitrate, which allows the encoder to work more space-efficiently (because portions of the audio that are less complex can be encoded at a lower bitrate). Because of this, the naive estimation doesn't work at all (unless, by coincidence, the bitrate of the first frame happens to be close to the average bitrate for the entire file). In order to accurately get the duration for a VBR file, we need to go through every single frame in the file and sum their individual durations. So that's what we're gonna do.
The overall structure of the MP3 decoder is going to be fairly similar to the ID3 one. We can even take advantage of the existing ID3 decoder to skip over the ID3 tag at the beginning of the file, thereby avoiding any false syncs (the `parse_tag` function needs to be amended to return the remaining binary data after the tag in order to do this). From there, it's simply a matter of scanning through the file looking for the magic sequence of 11 1-bits that mark the frame synchronization point and repeating that until we reach the end of the file.
@ -130,7 +130,7 @@ def parse_frame(...) do
end
```
We call the individual lookup functions for each of the pieces of data we need from the header. Using Elixir's `with` statement lets us pattern match on a bunch of values together. If any of the functions return `:invalid`, the pattern match will fail and it will fall through to the `else` part of thewith statement that matches anything. If that happens, we skip the first byte from the binary and recurse, looking for the next potential sync point.
We call the individual lookup functions for each of the pieces of data we need from the header. Using Elixir's `with` statement lets us pattern match on a bunch of values together. If any of the functions return `:invalid`, the pattern match will fail and it will fall through to the `else` part of the with statement that matches anything. If that happens, we skip the first byte from the binary and recurse, looking for the next potential sync point.
Inside the main body of the with statement, we need to find the number of samples in the frame.
@ -183,7 +183,7 @@ defp lookup_slot_size(:layer1), do: 4
defp lookup_slot_size(l) when l in [:layer2, :layer3], do: 1
```
One thing to note is that we `floor` the size before returning it. All of the division changes the value into a floating point, albeit one for which we know the decimal component will be zero. `floor`ing it turns it into an actual integer (`1` rather than `1.0`) because using a floating point value in a binary battern will cause an error.
One thing to note is that we `floor` the size before returning it. All of the division changes the value into a floating point, albeit one for which we know the decimal component will be zero. `floor`ing it turns it into an actual integer (`1` rather than `1.0`) because using a floating point value in a binary pattern will cause an error.
With that implemented, we can call it in the `parse_frame` implementation to get the number of bytes that we need to skip. We also perform the same calculation to get the frame duration. Then we can skip the requisite number of bytes of data, add the values we calculated to the various accumulators and recurse.

View File

@ -1,6 +1,6 @@
```
metadata.title = "Using lol-html (or any Rust crate) in Swift"
metadata.tags = ["swift"]
metadata.tags = ["swift", "rust"]
metadata.date = "2022-01-20 12:44:42 -0400"
metadata.shortDesc = "Works on real devices, the simulator, and Mac Catalyst."
metadata.slug = "swift-rust"

View File

@ -0,0 +1,183 @@
```
metadata.title = "Part 12: Typed Variables"
metadata.tags = ["build a programming language", "rust"]
metadata.date = "2022-05-25 16:38:42 -0400"
metadata.shortDesc = ""
metadata.slug = "typed-variables"
metadata.preamble = `<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>`
```
Hi. It's been a while. Though the pace of blog posts fell off a cliff last year[^1], I've continued working on my toy programming language on and off.
[^1]: During and after WWDC21, basically all of my non-work programming energy shifted onto iOS apps, and then never shifted back. I do recognize the irony of resuming mere weeks before WWDC22.
<!-- excerpt-end -->
## Part 1: Type Theory is for Chumps
I spent a while thinking about what I wanted the type system to look like—I do want some level of static typing, I know that much—but it got to the point where I was tired of thinking about it and just wanted to get back to writing code. So, lo and behold, the world's simplest type system:
```rust
#[derive(Debug, PartialEq, Clone, Copy)]
enum Type {
Integer,
Boolean,
String,
}
impl Type {
fn is_assignable_to(&self, other: &Type) -> bool {
self == other
}
}
```
Then, in the `Context`, rather than variables just being a map of names to `Value`s, the map now stores `VariableDecl`s:
```rust
struct VariableDecl {
variable_type: Type,
value: Value,
}
```
So variable declaration and lookup now goes through a simple helper in the function that creates the `VariableDecl`.
For now, types at variable declarations are optional at parse time since I haven't touched type inference yet and I didn't want to go back and update a bunch of unit tests. They are, however, inferred at evaluation time, if one wasn't specified.
```rust
fn parse_statement<'a, I: Iterator<Item = &'a Token>>(it: &mut Peekable<'a, I>) -> Option<Statement> {
// ...
let node = match token {
Token::Let => {
let name: String;
if let Some(Token::Identifier(s)) = it.peek() {
name = s.clone();
it.next();
} else {
panic!("expected identifier after let");
}
let mut variable_type = None;
if let Some(Token::Colon) = it.peek() {
it.next();
variable_type = Some(parse_type().expect("type after colon in variable declaration"));
}
expect_token!(it, Equals, "equals in variable declaration");
let value = parse_expression(it).expect("initial value in variable declaration");
Some(Statement::Declare {
name,
variable_type,
value,
})
}
// ...
};
// ...
}
```
The `parse_type` function is super simple, so I won't go over it—it just converts a the tokens for string/int/bool into their respective `Type`s. I call `expect` on the result of that type and then again wrap it in a `Some`, which seems redundant, because if whatever followed the colon wasn't a type, there's a syntax error and I don't want to continue.
Actually evaluating the variable declaration is still pretty straightforward, though it now checks that the type the initialization expression evaluated to matches the declared type:
```rust
fn eval_declare_variable(
name: &str,
mutable: bool,
variable_type: &Option<Type>,
value: &Node,
context: &ContextRef,
) {
let val = eval_expr(value, context);
let variable_type = match variable_type {
Some(declared) => {
assert!(
val.value_type().is_assignable_to(declared),
"variable value type is not assignable to declared type"
);
*declared
}
None => val.value_type(),
};
context
.borrow_mut()
.declare_variable(name, mutable, variable_type, val);
}
```
## Part 2: Variable Variables
The other bit I added was mutable variables, so that I could write a small program that did something non-trivial.
To do this, I changed the `VariableDecl` struct I showed above to hold a `ValueStorage` rather than a `Value` directly.
`ValueStorage` is an enum with variants for mutable and immutable variables. Immutables variables simply own their `Value`. Mutable ones, though, wrap it in a `RefCell` so that it can be mutated.
```rust
enum ValueStorage {
Immutable(Value),
Mutable(RefCell<Value>),
}
```
Setting the value is straightforward, but getting them is a bit annoying because `Value` isn't `Copy`, since it may own a string. So, there are a couple of helper functions: one to access the borrowed value and one to clone it.
```rust
impl ValueStorage {
fn set(&self, value: Value) {
match self {
ValueStorage::Immutable(_) => panic!("cannot set immutable variable"),
ValueStorage::Mutable(cell) => {
*cell.borrow_mut() = value;
}
}
}
fn with_value<R, F: FnOnce(&Value) -> R>(&self, f: F) -> R {
match self {
ValueStorage::Immutable(val) => f(&val),
ValueStorage::Mutable(cell) => f(&cell.borrow()),
}
}
fn clone_value(&self) -> Value {
self.with_value(|v| v.clone())
}
}
```
This works, but isn't ideal. At some point, the complex `Value` types should probably changed to reference-counted so, even if they're still not copy-able, cloning doesn't always involve an allocation.
Lexing and parsing I won't go into detail on, since it's trivial. There's a new for `var` and whether a declaration starts with that or `let` controls the mutability.
Setting variables isn't complicated either: when parsing a statement, if there's an equals sign after an identifier, that turns into a `SetVariable` which is evaluated simply by calling the aforementioned `set` function on the `ValueStorage` for that variable.
And with that, I can write a little fibonacci program:
```txt
$ cat fib.toy
var a = 0
var b = 1
var i = 0
while (i < 10) {
print("iteration: " + toString(i) + ", a: " + toString(a));
let tmp = a
a = b
b = tmp + a
i = i + 1
}
$ cargo r -- fib.toy
iteration: 0, a: 0
iteration: 1, a: 1
iteration: 2, a: 1
iteration: 3, a: 2
iteration: 4, a: 3
iteration: 5, a: 5
iteration: 6, a: 8
iteration: 7, a: 13
iteration: 8, a: 21
iteration: 9, a: 34
```
I also added a small CLI using [`structopt`](https://lib.rs/structopt) so I didn't have to keep writing code inside a string in `main.rs`.