For sale: baby jetpack. Worn once
For sale: baby jetpack. Worn once
It's been a while, maybe time to get back to computer and write something
Thatβs why it would only affect your followers β donβt follow people who broadcast wrong signals.
I think subtle negative signals would be more useful than generally imagined, but textual medias rarely use them. TikTok can detect when people skip videos, but itβs harder in these Twitter clones
What if we made a social network where instead of liking, blocking and muting, you would βhushβ and βboostβ posts?
Hushing would decrease visibility to your followers. Boosting would increase it.
Whoβs with me?
Tweet by @heydonworks that sparked some controversy in 2019: What do you like to do when you're not coding? Vue-based developer: I am fond of cooking, getting stuck into a good graphic novel andβ React-based developer: WEIGHTS. BROING DOWN. MORE WEIGHTS. TRUMP. WEIGHTS. GUNS. THE FREE MARKET.
People in Bluesky vs. people in Place Previously Known As Twitter
βCan you explain this gap in your resume?β
Yes, child; that was when I carried you
Thank you! Kissa is a many-splendored thing
Now I need to take a break of few days, weeks, or months, depending on when I next manage to free up some time. This was a miserable week for writing code: I had planned 5 full working days and got some 11+ hours done. But such is life sometimes
List of test instructions that ChatGPT wrote me. export const opcodes: { [key: number]: string } = { 0b00000000: "add", // add <op1> <op2> <op3> - op1 <- op2 + op3 0b00000001: "sub", // sub <op1> <op2> <op3> - op1 <- op2 - op3 0b00000010: "mul", // mul <op1> <op2> <op3> - op1 <- op2 * op3 // etc, ending with 0b00001100: "mov" 0b00010000: "adda", // add <op1> <op2> - op1 <- op1 + op2 0b00010001: "suba", // sub <op1> <op2> - op1 <- op1 - op2 0b00010010: "mula", // mul <op1> <op2> - op1 <- op1 * op2 // etc, ending with 0b00011100: "nop" 0b11110000: "hlt", // hlt <?> <?> <?> - halt program execution 0b11110001: "hcf", // hcf <?> <?> <?> - halt and catch fire }
Implementation of first 3 arithmetic instructions: if (instr === "add") { writeOperand(op1, op2value + op3value) } if (instr === "sub") { writeOperand(op1, op2value - op3value) } if (instr === "mul") { writeOperand(op1, op2value * op3value) } if (instr === "hlt") { return false } if (instr === "hcf") { error("Machine caught fire") } return true
test("arithmetic instructions", () => { testArithmetic(instr.add, 0x1234, 0x2345, 0x3579) testArithmetic(instr.sub, 0x4131, 0x2121, 0x2010) testArithmetic(instr.mul, 111, 20, 2220) testArithmetic(instr.mul, -111, 20, -2220) testArithmetic(instr.mul, -1, -0x7fff, 0x7fff) // TODO should overflow probably // testArithmetic(instr.mul, -1, -0x8000, -0x8000) })
Actually need to implement a bunch of arithmetic instructions and test them somehow efficiently. Here's a start.
Previous picture, plus new stuff (8-bit immediate instructions) // If bit 6 (immediate mode) is set, bit 7 sets "byte mode" // // Instruction names are postfixed with "b" for immediate. // Arithmetic instructions with 8-bit immediate operand (96-111): // // addb op1, op2, imm8 ; op1 <- op2 + imm8 01100000 // subb // mulb // divb // modb // andb // orb // xorb // shlb // shrb // add1b // sub1b // movb // // // Arithmetic instructions with 8-bit immediate operand, augmented assignment // (112-127): // // addab op1, imm8 ; op1 <- op2 + imm8 01110000 // subab // mulab // divab // modab // andab // orab // xorab // shlab // shrab // incab // decab // nopab
Here's the whole set of immediate instructions. This makes me reconsider the design, not only because of the effort of testing all of that stuff but also because I don't want to type "SHLAB" when I just want to do a 1-bit left shift. Maybe it's time to write some programs first and reconsider this.
I'm deliberately making a 1:1 mapping between names and opcodes to make assembling and disassembling easier. I may have to walk back on this decision later. It makes more complex names for often used instructions:
addai a, 1234 ; a: augmented assignment, b: immediate
addab a, 10 ; b: byte immediate
Immediate instructions with 16-bit operand (32-47), plus augmented ones (48-63) (Details omitted due to space constraints) addi op1, op2, imm16 ; op1 <- op2 + imm16 00100000 subi muli divi modi andi ori xori shli shri add1i sub1i movi addai op1, imm16 ; op1 <- op2 + imm16 00110000 subai mulai divai modai andai orai xorai shlai shrai incai decai nopai
Good two hours reserved for today, let's continue with immediate instructions. Since we have augmented mode too, there's quite a bunch of these.
Instructions that logically take only 1 argument don't make sense, such as INC/DEC, and some potential ones like NEG, NOT, CLR. Except for MOV.
I'm starting to think that maybe augmented assignment forms should be the default and the RISC-style 3 operand forms be the exception, since we want as short programs as possible. And generally shorter mnemonics as well. Maybe in the next version!
Augmented assignment instructions are derived from regular ones by appending "a" to the name, except for inc and dec (derived from add1 and sub1), and nop (derived from mov). Most instructions take 1 fewer parameter than the original one; nop, exceptionally, takes zero, for obvious reasons.
// // Augmented arithmetic instructions (16-31) // // adda op1, op2 ; op1 <- op2 + op3 00010000 // suba op1, op2 ; op1 <- op2 - op3 00010001 // mula op1, op2 ; op1 <- op2 * op3 00010010 // diva op1, op2 ; op1 <- op2 / op3 00010011 // moda op1, op2 ; op1 <- op2 % op3 00010100 // anda op1, op2 ; op1 <- op2 & op3 00010101 // ora op1, op2 ; op1 <- op2 | op3 00010110 // xora op1, op2 ; op1 <- op2 ^ op3 00010111 // shla op1, op2 ; op1 <- op2 << op3 00011000 // shra op1, op2 ; op1 <- op2 >> op3 00011001 // inc op1 ; op1 <- op2 + 1 00011010 - Note name // dec op1 ; op1 <- op2 - 1 00011011 - Note name // nop ; (do nothing) 00011100 - Note zero operands // // Augmented add1 is called inc. Augmented sub1 is called dec. Augmented mov // is called nop and takes 0 parameters. //
Next, we want "augmented assignment" forms of those instructions (such as "a += b"), and then also immediate forms taking either a 8-bit or a 16-bit operand. The complexity is quickly getting out of hand. But let's start with the augmented ones:
Notably missing: NOT (toggle all bits) and CLR. Though NOT is popular in many architectures, it's rarely used even in the kind of low-level programming I have in mind. CLR might find itself in the augmented instruction block (coming soon). Also absent: ROL and ROR, which might be fun for some uses.
Instructions Arithmetic instructions (0-15) add op1, op2, op3 ; op1 <- op2 + op3 00000000 sub op1, op2, op3 ; op1 <- op2 - op3 00000001 mul op1, op2, op3 ; op1 <- op2 * op3 00000010 div op1, op2, op3 ; op1 <- op2 / op3 00000011 mod op1, op2, op3 ; op1 <- op2 % op3 00000100 and op1, op2, op3 ; op1 <- op2 & op3 00000101 or op1, op2, op3 ; op1 <- op2 | op3 00000110 xor op1, op2, op3 ; op1 <- op2 ^ op3 00000111 shl op1, op2, op3 ; op1 <- op2 << op3 00001000 shr op1, op2, op3 ; op1 <- op2 >> op3 00001001 add1 op1, op2 ; op1 <- op2 + 1 00001010 sub1 op1, op2 ; op1 <- op2 - 1 00001011 mov op1, op2 ; op1 <- op2 00001100 Arithmetic instructions ending in 1101, 1110, 1111 are reserved. Some candidates: abs op1, op2 ; op1 <- abs(op2) sgn op1, op2 ; op1 <- sign(op2) neg op1, op2 ; op1 <- -op2 add2 op1, op2 ; op1 <- op2 + 2 sub2 op1, op2 ; op1 <- op2 - 2
Got a cup of coffee and made a plan for arithmetic instructions. Note the absence of unsigned operators like logical shift right, because who needs those when all words are signed?
Note that mov is considered one of the arithmetic opcodes (it's similar in structure to e.g. neg, just does nothing)
export const opcodes: { [key: number]: string } = { 0b00000000: "add", // add <dst> <src1> <src2> - dst <- src1 + src2 0b00000001: "sub", // sub <dst> <src1> <src2> - dst <- src1 - src2 0b11110000: "hlt", // hlt <?> <?> <?> - halt program execution 0b11110001: "hcf", // hcf <?> <?> <?> - halt and catch fire }
Day 3 and can't decide whether to take a nap or get a coffee. Good 2 hours to go and some code needs written. Maybe should expand this poor opcode list today
Here's the comment from 2022: github.com/nguillaumin/...
The 2002 (2004?) thing: nguillaumin.github.io/perihelion-m...
The 1994 thing can be found here: docs.dev-docs.org/htm/search.p...
In other news, while looking for some m68k wisdom, I found a historical gem where James Ingram, who wrote "The 68000 programmer's introduction to demo techniques" (1994) commented on perihelion's "The Atari ST MC68000 Assembly Language Tutorials" (2002) just one year ago and found his own font there
// Write 8-bit unsigned byte `byte` to memory address `addr`, or `more` after // that if provided export function write8(addr: number, byte: number, ...more: Array<number>) { if (byte < 0 || byte >= 256) { error(`write8: byte doesn't fit ${byte}`) } if (addr < 0 || addr >= MEM_SIZE) { error(`write8: invalid address ${addr}`) } mem[addr] = byte if (more.length) { for (let i = 0; i < more.length; i++) { mem[addr + i + 1] = more[i] } } }
test("writing bytes", () => { // add a.hi, b, c write8(0x100, instr.add, op.a_hi, op.b, op.c) // add a.lo, d, x write8(0x104, instr.add, op.a_lo, op.d, op.x) write8(0x108, instr.hlt) // Set initial values to registers and point PC to start write(addr.pc, 0x100) // Clear registers write(addr.a, 0) write(addr.b, 0x12) // 12 write(addr.c, 0x34) // + 34 -> 0x46__ write(addr.d, 0x56) // 56 write(addr.x, 0x78) // + 78 -> 0x__ce run() expect(read(addr.a)).toBe(0x46ce) })
Writing tests is more convenient by allowing write8() to take multiple arguments:
Code diff where "instructions." has been changed to "instr.", and registers like "A" to "op.a" when used as operand, but to "addr.a" when used as absolute address
Let's make things a bit more consistent. Left: before, right: after
export function step(): boolean { let pc: number = registers.pc log(`step: pc ${print(pc)}`) let opcode: number = read8(pc++) let instr: string = opcodes[opcode] ?? "nop" let op1: number = read8(pc++) let op2: number = read8(pc++) let op3: number = read8(pc++) registers.pc = pc let op2value = readOperand(op2) let op3value = readOperand(op3) log( `step: instr ${instr} ${print(opcode)} ${print(op1)} ${print(op2)} ${print( op3, )}`, ) if (instr === "add") { let result = op2value + op3value writeOperand(op1, result) return true } if (instr === "hlt") { return false } if (instr === "hcf") { error("Machine caught fire") } return true }
And here is the core of the virtual machine:
function readOperand(op: number) { let lowest6bits = op & 0b0011_1111 if (op & 0b0100_0000) { return read8s(lowest6bits) } else { return read(lowest6bits) } } function writeOperand(op: number, value: number) { let lowest6bits = op & 0b0011_1111 if (op & 0b0100_0000) { write8(lowest6bits, value) } else { write(lowest6bits, value) } }
export function read8s(addr: number) { if (addr < 0 || addr >= MEM_SIZE) { error(`read8: invalid address ${addr}`) } let result = mem[addr] if ((result & 0b1000_0000) > 0) { // Sign-extend return result | 0xffffff00 } else { return result } }
To read and write bytes we introduce readOperand() and writeOperand(), which are used to do the heavy lifting. We also need a function to read and sign-extend a byte, read8s(). Extra write function is not needed, write8 works for signed bytes as well.
export function hi(op: number): number { return op | 0b0100_0000 } export function lo(op: number): number { return (op + 1) | 0b0100_0000 }
hi() and lo() set the 7th bit of the operand, which turns on byte addressing (as opposed to word addressing, which is the default)
test("writing bytes", () => { // add a.lo, b, c write8(0x100, instructions.add) write8(0x101, hi(A)) write8(0x102, B) write8(0x103, C) // add a.hi, d, x write8(0x104, instructions.add) write8(0x105, lo(A)) write8(0x106, D) write8(0x107, X) write8(0x108, instructions.hlt) // Set initial values to registers and point PC to start write(PC, 0x100) // Clear registers write(A, 0) write(B, 0x12) // 12 write(C, 0x34) // + 34 -> 0x46__ write(D, 0x56) // 56 write(X, 0x78) // + 78 -> 0x__ce run() expect(read(A)).toBe(0x46ce) })
After a bit of byte wrangling, we can now write and read bytes (which, upon reading, are sign-extended)
β jump relative with add [0.03ms] step: pc 0x0100 step: instr add (0x0000) step: pc 0x0104 step: instr hlt (0x00f0) β program with byte addressing [0.08ms]
test("program with byte addressing", () => { // add a, a, c.hi write8(0x100, instructions.add) write8(0x101, A) write8(0x102, A) write8(0x103, hi(C)) // add b, c, c.lo write8(0x104, instructions.add) write8(0x105, B) write8(0x106, B) write8(0x107, lo(C)) write8(0x104, instructions.hlt) // Set initial values to registers and point PC to start write(PC, 0x100) // C contains high byte 0x1f, and low byte 0xf1 write(C, 0x1ff1) debugOn() run() debugOff() // expect(read(A)).toBe(0x1f) // B should be sign-extended // expect(read(B)).toBe(0xfff1) })
Added some printf debugging machinery to find out why my second add instruction didn't run, can you guess why?
Autumn
Day 2 of virtual machine hacking, think Iβll do byte addressing now
Operand format: Direct addressing (word), values 0..63: 00mmmmmm: [M], where M is number 0-63 Direct addressing (byte), values 64..127: 01mmmmmm: [M].b, where M is number 0-63 Indirect addressing with immediate offset (word), values 128..175: 10ooooaa: [<rega> + O], where <rega> is selected with bits 'aa' and O is the range 0b0000-0b1011, represented by bits 'oooo', mapped to values -6 to 5 Indirect addressing with index register (word), values 176..191: 1011iiaa: [<rega> + <regi>], where <rega> is selected with bits 'aa' and <regi> is selected with bits 'ii' Indirect addressing with immediate offset (byte), values 192..239: 11ooooaa: [<rega> + O].b, where <rega> is selected with bits 'aa' and O is range 0b0000-0b1011 mapped to values -6 to 5 Indirect addressing with index register (byte), values 240..255: 1111iiaa: [<rega> + <regi>].b, where <rega> is selected with bits 'aa' and <regi> is selected with bits 'ii'
What we have now is direct addressing, e.g. [0x1234]. This extends the design with indirect addressing using either immediate or an index register, allowing things like [pc + 5] and [p + i]. Also byte addressing is supported with syntax [0x1234].b
Syntax [M] means the word at memory address M. [M].b means the byte at memory address M. When read, it is sign-extended to a word. <reg> is equivalent to [addressof <reg>], where <reg> is one of registers (a, b, c, d, x, y, i, j, p, q, sp, pc), and addressof <reg> is the memory address of the register according to the table below. <reg>.hi is equivalent to [address of <reg>].b <reg>.lo is equivalent to [address of <reg> + 1].b Table of registers and their addresses: a: 0x0 a.hi: 0x0 a.lo: 0x1 b: 0x2 b.hi: 0x2 b.lo: 0x3 c: 0x4 c.hi: 0x4 c.lo: 0x5 d: 0x6 d.hi: 0x6 d.lo: 0x7 x: 0x8 x.hi: 0x8 x.lo: 0x9 y: 0xa y.hi: 0xa y.lo: 0xb i: 0xc i.hi: 0xc i.lo: 0xd j: 0xe j.hi: 0xe j.lo: 0xf p: 0x10 p.hi: 0x10 p.lo: 0x11 q: 0x12 q.hi: 0x12 q.lo: 0x13 sp: 0x14 sp.hi: 0x14 sp.lo: 0x15 pc: 0x16 pc.hi: 0x16 pc.lo: 0x17 Finally: <regi> is one of x, y, i, j <rega> is one of p, q, sp, pc
Operands are now simply 8 bits and denote memory addresses 0x00 to 0xff. I have a slightly more complex design in mind but let's get some syntax defined first. This also details how to address lower and higher parts of registers: