ไม่
ก
N และขับเคลื่อน
อุปกรณ์
เป็นคอมพิวเตอร์ 16 บิตที่เทียบเท่ากับทัวริงที่สร้างขึ้นจากนาฬิกาและเกท NAND ที่จำลองบนเว็บ NAND มี CPU ของตัวเอง, ภาษารหัสเครื่อง, ภาษาแอสเซมบลี, แอสเซมเบลอร์, ภาษาเครื่องเสมือน, เครื่องแปลเครื่องเสมือน, ภาษาการเขียนโปรแกรม, คอมไพเลอร์, IDE และอินเทอร์เฟซผู้ใช้ NAND ใช้แพลตฟอร์ม Jack-VM-Hack ที่ระบุในหลักสูตร Nand to Tetris และหนังสือที่เกี่ยวข้อง
โปรแกรมง่ายๆ ที่ป้อนตัวเลขจำนวนหนึ่งและคำนวณค่าเฉลี่ย แสดงโฟลว์การควบคุม การดำเนินการทางคณิตศาสตร์ I/O และการจัดสรรหน่วยความจำแบบไดนามิก
ผลลัพธ์ของโปรแกรม:
How many numbers? 4
Enter a number: 100
Enter a number: 42
Enter a number: 400
Enter a number: 300
The average is 210
โปรแกรมนี้จัดทำโดยชุดซอฟต์แวร์ Nand ถึง Tetris
เกมโป่ง โชว์โมเดลเชิงวัตถุของภาษา ใช้ปุ่มลูกศรเพื่อเลื่อนไม้พายไปทางซ้ายและขวาเพื่อตีกลับลูกบอล ทุกครั้งที่กระเด้ง ไม้พายจะเล็กลง และเกมจะจบลงเมื่อลูกบอลกระทบด้านล่างของหน้าจอ
โปรแกรมนี้จัดทำโดยชุดซอฟต์แวร์ Nand ถึง Tetris
เกมปี 2048 แสดงให้เห็นถึงการเรียกซ้ำและตรรกะการใช้งานที่ซับซ้อน ใช้ปุ่มลูกศรเพื่อย้ายตัวเลขรอบๆ ตาราง 4x4 ตัวเลขเดียวกันจะรวมกันเป็นผลรวมเมื่อย้ายเข้ามาหากัน เมื่อถึงไทล์ 2048 คุณจะชนะเกม แต่คุณสามารถเล่นต่อได้จนกว่าคุณจะแพ้ คุณจะแพ้เกมเมื่อกระดานเต็ม และคุณไม่สามารถเคลื่อนไหวได้อีก
โปรแกรมที่จงใจทำให้เกิดสแต็กโอเวอร์โฟลว์ผ่านการเรียกซ้ำแบบไม่สิ้นสุดเพื่อดำเนินการหลีกเครื่องเสมือน มันใช้ประโยชน์จากข้อเท็จจริงที่ว่าไม่มีการตรวจสอบรันไทม์เพื่อป้องกันสแต็กโอเวอร์โฟลว์ ไม่มีแพลตฟอร์มสมัยใหม่อื่นใดที่จะให้คุณทำเช่นนี้ :-)
เมื่อรันโปรแกรมจะพิมพ์ตัวชี้สแต็กไปที่หน้าจออย่างต่อเนื่อง เมื่อค่าที่แสดงนี้เกิน 2048 สแต็กจะถึงจุดสิ้นสุดของพื้นที่หน่วยความจำที่ต้องการและกระจายไปยังพื้นที่หน่วยความจำฮีป ส่งผลให้คำสั่งการพิมพ์ทำงานผิดปกติในรูปแบบที่ระเบิดได้:
มีสองสิ่งที่น่าสนใจน่าสังเกตที่ควรค่าแก่การชี้ให้เห็น
หากคุณรันโปรแกรมนี้บน RAM ว่างซึ่งเต็มไปด้วยเลขศูนย์ (คุณสามารถล้าง RAM ผ่านทางอินเทอร์เฟซผู้ใช้) คุณจะสังเกตเห็นว่าโปรแกรมรีเซ็ตตัวเองได้ครึ่งทางของการดำเนินการแม้ว่าจะไม่ได้กดปุ่ม "รีเซ็ต" ก็ตาม เหตุใดสิ่งนี้จึงเกิดขึ้นได้ง่าย: รันไทม์เจลเบรครันคำสั่งที่ตั้งค่าตัวนับโปรแกรมเป็น 0 โดยบอกให้โปรแกรมข้ามไปที่คำสั่งแรกและเริ่มต้นใหม่อย่างมีประสิทธิภาพ
หากคุณรันโปรแกรมตัวอย่าง GeneticAlgorithm แล้วรันทันทีหลังจากนั้น โปรแกรมที่อยู่ในอาละวาดจะอ่านหน่วยความจำ RAM เก่าที่ไม่เคยเขียนทับเลย
โปรแกรมที่ใช้ประโยชน์จากข้อเท็จจริงที่ว่ารันไทม์ไม่ได้ป้องกันการแตกของสแต็กเพื่อเรียกใช้ฟังก์ชันที่ไม่สามารถเข้าถึงได้ เพื่อให้เข้าใจถึงวิธีการทำงาน เรามาดูภาพประกอบเค้าโครงเฟรมสแต็กของ NAND กัน
นำมาจากหนังสือ Nand สู่ Tetris
หากคุณไม่คุ้นเคยกับรูปแบบสแต็ก นี่คือแนวคิดหลักเบื้องหลังการหาประโยชน์นี้ เมื่อใดก็ตามที่ฟังก์ชันกลับมา จะต้องรู้ว่าควรไปที่ใด (ที่อยู่หน่วยความจำคำสั่งรหัสเครื่องใด) เพื่อดำเนินการโฟลว์การดำเนินการต่อไป ดังนั้นเมื่อมีการเรียกใช้ฟังก์ชันเป็นครั้งแรก ที่อยู่หน่วยความจำนี้พร้อมกับข้อมูลที่ไม่สำคัญอื่นๆ จะถูกจัดเก็บไว้ชั่วคราวบนสแต็กในพื้นที่หน่วยความจำที่เรียกว่าเฟรมสแต็กเพื่อเป็นข้อมูลอ้างอิงสำหรับตำแหน่งที่จะส่งคืน ภาพประกอบจะอธิบายตำแหน่งที่แน่นอนของที่อยู่ผู้ส่งนี้สัมพันธ์กับการเรียกใช้ฟังก์ชัน ซึ่งเป็นตำแหน่งที่สามารถวิศวกรรมย้อนกลับได้
โปรแกรมช่วยให้ผู้ใช้สามารถเขียนทับที่อยู่หน่วยความจำเดียวใน RAM เป็นค่าใดก็ได้ เมื่อรวมสองและสองเข้าด้วยกัน หากผู้ใช้เขียนทับที่อยู่ผู้ส่งของเฟรมสแต็กด้วยที่อยู่ของฟังก์ชันอื่น พวกเขาจะได้รับความสามารถในการรันโค้ดตามอำเภอใจที่รวมอยู่ในโปรแกรม
แท้จริงแล้ว หากคุณป้อน 267 เป็นตำแหน่งหน่วยความจำ และ 1715 เป็นค่าที่จะเขียนทับ ตัวเลขสองตัวที่วิศวกรรมย้อนกลับโดยการตรวจสอบพื้นที่หน่วยความจำสแต็กและแอสเซมเบลอร์ด้วยตนเอง คุณจะเห็นแนวคิดนี้ในการทำงาน
นี่ไม่ใช่ช่องโหว่เฉพาะของ NAND มันมีอยู่ใน C เช่นกัน! เจ๋งแค่ไหน!
เชื่อหรือไม่ว่าในบรรดาส่วนประกอบต่างๆ มากมาย ของ NAND การดำเนินการนี้ใช้เวลาเพียงลำพังในการพัฒนานานที่สุด!
โปรแกรมนี้เป็นการจำลองสิ่งมีชีวิตที่ใช้การเรียนรู้ของเครื่องอย่างง่าย เป็นไปตามซีรีส์รหัสปัญญาประดิษฐ์ (ตอนที่หนึ่งและสอง) จาก Code Bullet อย่าลืมเข้าไปดูช่องของเขาด้วย เขาทำสิ่งเจ๋งๆ มากมาย!
อธิบายง่ายๆ:
ทุกจุดมี "สมอง" ของเวกเตอร์ความเร่งของตัวเอง และพวกมันจะพัฒนาเพื่อบรรลุเป้าหมายผ่านการคัดเลือกโดยธรรมชาติ ในแต่ละรุ่น จุดที่ "ตาย" ใกล้เป้าหมายมากขึ้น มีแนวโน้มที่จะถูกเลือกให้เป็นผู้ปกครองสำหรับคนรุ่นต่อไป การสืบพันธุ์ทำให้สมองบางส่วนกลายพันธุ์โดยธรรมชาติ ซึ่งเป็นการจำลองวิวัฒนาการทางธรรมชาติอย่างมีประสิทธิภาพทั้งหมด
อย่างไรก็ตาม ยังมีสิ่งที่ต้องการอีกมากมาย เนื่องจากประสิทธิภาพ ปัจจัยเดียวที่จุดใช้ในการพัฒนาคือความใกล้ชิดกับเป้าหมายหลังความตาย ส่งผลให้อัลกอริธึมการคัดเลือกโดยธรรมชาติมีเอนโทรปีต่ำ เนื่องจากการใช้หน่วยความจำ จึงมีจำนวนจุดและขนาดของสมองน้อยกว่าขีดจำกัดที่น่าพอใจ สุดท้ายนี้ เนื่องจากความซับซ้อนทางเทคนิค การวางสิ่งกีดขวางใหม่ระหว่างการจำลองไม่ได้รับประกันว่าจุดต่างๆ จะมีสมองที่ใหญ่พอที่จะบรรลุเป้าหมาย ขนาดสมองจะถูกกำหนดเมื่อเริ่มต้นโปรแกรมเท่านั้น
ฉันใช้เทคนิคการปรับให้เหมาะสมมากมายเพื่อเลี่ยงข้อจำกัดด้านฮาร์ดแวร์ต่อไปนี้และทำให้สิ่งนี้เป็นไปได้:
เพื่อหลีกเลี่ยงการยุ่งวุ่นวาย ฉันจึงพยายามบันทึกเทคนิคเหล่านี้และข้อมูลเชิงลึกเพิ่มเติมใน Codebase ของโปรแกรมนี้สำหรับผู้ที่สนใจ
ก่อนที่เราจะเริ่มต้น รายละเอียดที่สำคัญที่สุดที่ต้องจำเกี่ยวกับการเขียนโปรแกรมใน Jack ก็คือ ไม่มีลำดับความสำคัญของผู้ปฏิบัติงาน นี่อาจเป็นสาเหตุที่โปรแกรมของคุณไม่ทำงาน
ตัวอย่างเช่น คุณควรเปลี่ยน:
4 * 2 + 3
ถึง (4 * 2) + 3
if (~x & y)
ถึง if ((~x) & y)
แต่คุณสามารถคงไว้ได้ if (y & ~x)
เหมือนเดิมเนื่องจากไม่มีตัวดำเนินการคลุมเครือ
หากไม่มีวงเล็บ ค่าประเมินของนิพจน์ที่ไม่ชัดเจนจะ ไม่ได้กำหนดไว้
NAND มีกลุ่มเทคโนโลยีที่สมบูรณ์ของตัวเอง ด้วยเหตุนี้จึงสามารถตั้งโปรแกรม NAND ได้ใน Jack เท่านั้น ซึ่งเป็นภาษาการเขียนโปรแกรมเชิงวัตถุที่พิมพ์ไม่ชัดเจน ในแง่ของคนธรรมดา Jack คือ C พร้อมด้วยไวยากรณ์ของ Java
มาดูแนวทางการเรียนรู้ตามตัวอย่างและเจาะลึกกัน
/**
* This program prompts the user to enter a phrase
* and an energy level. Program output:
*
* Whats on your mind? Superman
* Whats your energy level? 3
* Superman!
* Superman!
* Superman!
*/
class Main {
function void main ( ) {
var String s ;
var int energy , i ;
let s = Keyboard . readLine ( "Whats on your mind? " ) ;
let energy = Keyboard . readInt ( "Whats your energy level? " ) ;
let i = 0 ;
let s = s . appendChar ( 33 ) ; // Appends the character '!'
while ( i < energy ) {
do Output . printString ( s ) ;
do Output . println ( ) ;
let i = i + 1 ;
}
}
}
นำมาจากสไลด์บรรยาย Nand ไปจนถึง Tetris
หากคุณมีประสบการณ์เกี่ยวกับการเขียนโปรแกรมมาบ้างแล้ว สิ่งนี้น่าจะค่อนข้างคุ้นเคย เห็นได้ชัดว่าแจ็คได้รับแรงบันดาลใจอย่างมากจาก Java Main.main
ซึ่งเป็นจุดเริ่มต้นของโปรแกรม สาธิตการใช้งานพื้นฐานของตัวแปร เช่นเดียวกับการวนซ้ำ while สำหรับโฟลว์การควบคุม
นอกจากนี้ ยังใช้ Keyboard.readLine
และ Keyboard.readInt
เพื่ออ่านอินพุตจากผู้ใช้ และ Output.printString
และ Output.println
เพื่อพิมพ์เอาต์พุตไปยังหน้าจอ ซึ่งทั้งหมดนี้ถูกกำหนดไว้โดยละเอียดใน Jack OS Reference ตามค่าเริ่มต้น Jack OS จะรวมเข้ากับโปรแกรมของคุณในระหว่างการคอมไพล์เพื่อให้สามารถเชื่อมต่อกับสตริง หน่วยความจำ ฮาร์ดแวร์ และอื่นๆ ได้
ภาษาการเขียนโปรแกรมทุกภาษามีชุดข้อมูลพื้นฐานคงที่ แจ็ครองรับสาม: int
, char
และ boolean
คุณสามารถขยายรายการพื้นฐานนี้ด้วยประเภทข้อมูลนามธรรมของคุณเองได้ตามต้องการ ความรู้ก่อนหน้าเกี่ยวกับการเขียนโปรแกรมเชิงวัตถุจะส่งต่อไปยังส่วนนี้โดยตรง
/** Represents a point in 2D plane. */
class Point {
// The coordinates of the current point instance:
field int x , y ;
// The number of point objects constructed so far:
static int pointCount ;
/** Constructs a point and initializes
it with the given coordinates */
constructor Point new ( int ax , int ay ) {
let x = ax ;
let y = ay ;
let pointCount = pointCount + 1 ;
return this ;
}
/** Returns the x coordinate of the current point instance */
method int getx ( ) { return x ; }
/** Returns the y coordinate of the current point instance */
method int gety ( ) { return y ; }
/** Returns the number of Points constructed so far */
function int getPointCount ( ) {
return pointCount ;
}
/** Returns a point which is this
point plus the other point */
method Point plus ( Point other ) {
return Point . new ( x + other . getx ( ) ,
y + other . gety ( ) ) ;
}
/** Returns the Euclidean distance between the
current point instance and the other point */
method int distance ( Point other ) {
var int dx , dy ;
let dx = x - other . getx ( ) ;
let dy = y - other . gety ( ) ;
return Math . sqrt ( ( dx * dx ) + ( dy * dy ) ) ;
}
/** Prints the current point instance, as "(x, y)" */
method void print ( ) {
var String tmp ;
let tmp = "(" ;
do Output . printString ( tmp ) ;
do tmp . dispose ( ) ;
do Output . printInt ( x ) ;
let tmp = ", " ;
do Output . printString ( tmp ) ;
do tmp . dispose ( ) ;
do Output . printInt ( y ) ;
let tmp = ")" ;
do Output . printString ( tmp ) ;
do tmp . dispose ( ) ;
}
}
var Point p1 , p2 , p3 ;
let p1 = Point . new ( 1 , 2 ) ;
let p2 = Point . new ( 3 , 4 ) ;
let p3 = p1 . plus ( p2 ) ;
do p3 . print ( ) ; // prints (4, 6)
do Output . println ( ) ;
do Output . printInt ( p1 . distance ( p2 ) ) ; // prints 5
do Output . println ( ) ;
do Output . printInt ( getPointCount ( ) ) ; // prints 3
นำมาจากสไลด์บรรยาย Nand ไปจนถึง Tetris
เรากำหนดคลาส Point
เพื่อเป็นตัวแทนของจุดนามธรรมในอวกาศ ใช้ตัวแปร field
เพื่อประกาศแอตทริบิวต์ต่ออินสแตนซ์ของประเภทข้อมูล มันเปิดเผยฟังก์ชัน method
สาธารณะที่เราสามารถใช้เพื่อเชื่อมต่อกับจุด ทำให้ผู้เรียกมีฟังก์ชันในการเพิ่มจุดสองจุดเข้าด้วยกันและคำนวณระยะห่างระหว่างจุดสองจุด
ตัวแปร field
ทั้งหมดมีการกำหนดขอบเขตแบบส่วนตัว หากคุณต้องการรับหรือตั้งค่าตัวแปรเหล่านี้จากภายนอกการประกาศคลาส ตัวแปรเหล่านี้จะต้องมีฟังก์ชัน method
ที่สอดคล้องกันเพื่อให้ฟังก์ชันการทำงานนี้
ละเว้นจากตัวอย่างโค้ดเพื่อให้อยู่ในหัวข้อ เป็นธรรมเนียมที่คลาสข้อมูลจะกำหนดวิธี dispose
สำหรับการจัดสรรคืนเมื่อไม่ต้องการออบเจ็กต์อีกต่อไป ดูการจัดการหน่วยความจำด้วยตนเอง
หากจำเป็น นี่คือข้อมูลอ้างอิงสำหรับไวยากรณ์การเรียกใช้ function
และ method
class Foo {
...
method void f ( ) {
var Bar b ; // Declares a local variable of class type Bar
var int i ; // Declares a local variable of primitive type int
do g ( ) ; // Calls method g of the current class on the current object instance
// Note: Cannot be called from within a function (static method)
do Foo . p ( 3 ) ; // Calls function p of the current class;
// Note: A function call must be preceded by the class name
do Bar . h ( ) ; // Calls function h of class Bar
let b = Bar . r ( ) ; // Calls function or constructor r of class Bar
do b . q ( ) ; // Calls method q of class Bar on the b object
}
}
นำมาจากสไลด์บรรยาย Nand ไปจนถึง Tetris
จำที่เราบอกว่าแจ็คมีความคล้ายคลึงกับ Java ได้ไหม? นั่นเป็นส่วนหน้าหรือที่ทำให้เข้าใจผิด แม้ว่า Java จะถูกพิมพ์อย่างเข้มงวดและสนับสนุนคุณสมบัติประเภทที่ซับซ้อน เช่น downcast, polymorphism และการสืบทอด แต่ Jack ไม่สนับสนุนสิ่งเหล่านี้เลย และมีเพียงประเภทเดียวภายใต้ประทุน: จำนวนเต็ม 16 บิตที่เซ็นชื่อ นี่คือเหตุผลหลักว่าทำไมแจ็คถึงพิมพ์ไม่ค่อยเก่ง คอมไพลเลอร์จะไม่สนใจว่าคุณผสมและจับคู่ประเภทต่างๆ ในการกำหนดและการดำเนินการหรือไม่
var char c ;
var String s ;
let c = 65 ; // 'A'
// Equivalently
let s = "A" ;
let c = s . charAt ( 0 ) ;
var Array a ;
let a = 5000 ;
let a [ 100 ] = 77 ; // RAM[5100] = 77
var Array arr ;
var String helloWorld ;
let helloWorld = "Hello World!"
let arr = Array . new ( 4 ) ; // Arrays are not strictly typed
let arr [ 0 ] = 12 ;
let arr [ 1 ] = false ;
let arr [ 2 ] = Point . new ( 5 , 6 ) ;
let arr [ 3 ] = helloWorld ;
class Complex {
field int real ;
field int imaginary ;
...
}
. . .
var Complex c ;
var Array a ;
let a = Array . new ( 2 ) ;
let a [ 0 ] = 7 ;
let a [ 1 ] = 8 ;
let c = a ; // c == Complex(7, 8)
// Works because it matches the memory layout
// of the Complex type
ส่วนโค้ดทั้งหมดที่นำมาจากสไลด์บรรยาย Nand ถึง Tetris
อย่าใช้วิธีนี้ในทางที่ผิด — Jack ยังคงนำเสนอโมเดลเชิงวัตถุที่ทรงพลังและใช้งานได้จริง ข้อมูลเชิงลึกนี้มีจุดมุ่งหมายเพื่อช่วยให้คุณเข้าใจว่าคุณควรดำเนินการแปลงประเภทเมื่อใดและอย่างไรตามความจำเป็น
สมมติว่าคุณเป็นคนรักแมวบ้าเหมือนฉัน! และคุณอยากจะเขียนโปรแกรมนี้เพื่ออวดว่าคุณรักแมวมากแค่ไหน
class Main {
function void main ( ) {
while ( true ) {
do Output . printString ( "Kittens are so adorable! " ) ;
}
}
}
คุณอาจตกใจเมื่อสังเกตเห็นว่าหลังจากนั้นไม่กี่วินาที โปรแกรมจะขัดข้องด้วย "ERR6" หรือมีฮีปโอเวอร์โฟลว์!
แจ็คเป็นภาษาโปรแกรมที่จัดการหน่วยความจำด้วยตนเอง ซึ่งหมายความว่าคุณจะต้องระมัดระวังในการจัดสรรหน่วยความจำที่ไม่จำเป็นอีกต่อไป มิฉะนั้น Jack OS จะคิดอย่างอื่นและช่วยให้หน่วยความจำรั่วไหล คำแนะนำแนวทางปฏิบัติที่ดีที่สุดคือการนำเสนอวิธี dispose
สำหรับแต่ละคลาสที่แสดงถึงออบเจ็กต์ที่ห่อหุ้มการจัดสรรคืนนี้อย่างเหมาะสม ดังนั้น เมื่อไม่ต้องการอ็อบเจ็กต์อีกต่อไป คุณสามารถเรียกใช้เมธอด dispose
เพื่อให้แน่ใจว่าหน่วยความจำฮีปจะไม่หมดในที่สุด
หากคุณได้ตั้งโปรแกรมในภาษาอื่นๆ ที่จัดการหน่วยความจำด้วยตนเอง เช่น C สิ่งนี้น่าจะดูคุ้นเคยดี ข้อแตกต่างที่สำคัญประการหนึ่งคือ Jack OS จะจัดเก็บอาร์เรย์และสตริงบนฮีปแทนที่จะเก็บบนสแต็ก ซึ่งบอกเป็นนัยว่าเหตุใดโปรแกรมจึงขัดข้องเนื่องจากมีฮีปโอเวอร์โฟลว์
มาแก้ไขโปรแกรมนี้เพื่อผู้คลั่งไคล้แมวของเรากันเถอะ
class Main {
function void main ( ) {
var String s ;
while ( true ) {
let s = "Kittens are so adorable! " ;
do Output . printString ( s ) ;
do s . dispose ( ) ;
}
}
}
หรือคุณสามารถจัดสรรหน่วยความจำสำหรับสตริงได้เพียงครั้งเดียวเท่านั้น
class Main {
function void main ( ) {
var String s ;
let s = "Kittens are so adorable! " ;
while ( true ) {
do Output . printString ( s ) ;
}
}
}
คุณจะสังเกตเห็นว่าไม่เพียงแต่เวอร์ชันทางเลือกเหล่านี้พิมพ์สตริงได้เร็วกว่ามาก แต่คราวนี้พวกเขาจะพิมพ์ตลอดไปจริงๆ! ไชโย!
มาดู String.dispose
กันอย่างรวดเร็วเพื่อให้คุณเข้าใจวิธีเขียนวิธี dispose
ของคุณเองได้ดีขึ้น
method void dispose ( ) {
do stringArray . dispose ( ) ;
do Memory . deAlloc ( this ) ;
}
Array.dispose
เรียกโดย stringArray
method void dispose ( ) {
do Memory . deAlloc ( this ) ;
}
วิธี dispose
ที่เหมาะสมจะต้องเรียก dispose
อย่างเหมาะสมกับตัวแปรฟิลด์ จากนั้นจึงจบด้วย do Memory.deAlloc(this);
เพื่อจัดสรรคืนอินสแตนซ์ของวัตถุเอง
เนื่องจาก Jack และ NAND มีความดั้งเดิมเพียงใด ปืนลูกซองในภาษานี้จึงเป็นสิ่งที่หลีกเลี่ยงไม่ได้ ฉันได้รวบรวมตัวอย่างพฤติกรรมที่ไม่ได้กำหนดไว้ต่อไปนี้ที่คุณควรทราบ โดยเรียงลำดับจาก (ในความคิดของฉัน) สำคัญที่สุดไปหาสำคัญน้อยที่สุด
ฉันพบว่าคำเตือนนี้สำคัญมากจนฉันย้ายไปยังจุดเริ่มต้นของส่วนนี้
การแสดงออกของแจ็ค
a > b
a < b
เรียบง่ายอย่างหลอกลวง สิ่งเหล่านี้ไม่ได้ถูกต้องตามหลักคณิตศาสตร์เสมอไป และเทียบเท่ากับนิพจน์ Java ตามลำดับ
( ( a - b ) & ( 1 << 15 ) ) == 0 && a != b
( ( a - b ) & ( 1 << 15 ) ) != 0
เกิดอะไรขึ้นกับความแตกต่างกันนิดหน่อย? การใช้งานเครื่องเสมือนแปลง a > b
เป็น a - b > 0
นี่คือปัญหา: a - b
สามารถล้นได้ :(
20000 > -20000
ประเมินเป็นเท่าใด เครื่องเสมือนแปลงสิ่งนี้เป็น 20000 - (-20000) > 0
ซึ่งประเมินเป็น -25336 > 0
น่าเสียดายที่คำตอบคือ false
อย่างไรก็ตาม 20000 > -10000
ประเมินเป็น 30000 > 0
หรือ true
ดังที่คุณคงทราบแล้วว่า ถ้าระยะห่างสัมบูรณ์ระหว่าง a
และ b
มากกว่า 32767 a > b
และ a < b
นั้นผิด มิฉะนั้นคุณก็สบายดี
นี่ไม่ใช่ข้อบกพร่องในการใช้งาน แต่เป็นความไม่สอดคล้องกับ Nand ถึง Tetris เอง ข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ที่นี่ ด้วยเหตุผลด้านความเข้ากันได้ พฤติกรรมนี้จะไม่ได้รับการแก้ไข
-32768 เป็นหนึ่งในประเภทเดียวกัน เป็นตัวเลขเดียวที่มีคุณสมบัติดังกล่าวว่า -(-32768) = -32768 ซึ่งเป็นซิงเกิลตันที่ไม่มีคู่ที่เป็นบวก * สิ่งนี้สามารถนำไปสู่ความไม่มั่นคงและข้อผิดพลาดทางตรรกะ
/**
* Program output:
* --.)*(
*/
class Main {
function void main ( ) {
// Note that -32768 must instead be written as ~32767
// because the CPU can't load a number that large
do Output . printInt ( ~ 32767 ) ;
}
}
Output.printInt
ภายในคาดว่า Math.abs
จะส่งกลับจำนวนบวก นี่ไม่ใช่กรณีของ -32768 ดังนั้น Jack OS จึงทำงานผิดปกติ
ข้อกังวลหลักของคุณควรจัดการกับข้อผิดพลาดทางตรรกะด้วยตัวดำเนินการเชิงลบ ในฐานะโปรแกรมเมอร์ หากคุณต้องการรับประกันว่าค่าลบของจำนวนลบนั้นเป็นค่าบวก คุณมีหน้าที่ตรวจสอบกรณีของ -32768 และดำเนินการตามความเหมาะสม
* สิ่งนี้ถือเป็นจริงเนื่องจาก ALU ของ NAND ประมวลผลนิพจน์ Jack -x
เป็น ~(x - 1)
ภายใน ลองตั้ง x
เป็น -32768
และประเมินทีละขั้นตอน ต่อไปนี้เป็นการแสดงไบนารีเสริมของการคำนวณแบบ 16 บิตสอง:
x
= 1000 0000 0000 0000
x - 1
= 0111 1111 1111 1111
~(x - 1)
= 1000 0000 0000 0000
= x
มันเป็นเรื่องเดียวกัน! เกิดอะไรขึ้นที่นี่? เนื่องจาก NAND เป็นเครื่อง 16 บิต ดังนั้น -32768 จึงเป็นตัวเลขเดียวที่หากคุณลบออกหนึ่งตัว คุณจะได้บิตที่กลับด้าน กล่าวอีกนัยหนึ่ง -32768 เป็นไปตาม x - 1 = ~x
ทำให้การแสดงออกง่ายขึ้นเป็น ~(~x)
หรือเพียง x
อันนี้อธิบายได้ในตัว ดังนั้นนี่คือการสาธิตสั้นๆ
/**
* Program output:
* I have 818 cookies.
*/
class Main {
function void main ( ) {
do Main . cookies ( ) ;
}
function void cookies ( int a ) {
do Output . printString ( "I have " ) ;
do Output . printInt ( a ) ;
do Output . printString ( " cookies." ) ;
}
}
ในทางกลับกัน การเรียกใช้ฟังก์ชันที่มีอาร์กิวเมนต์ มาก เกินไปถือว่าถูกต้องสมบูรณ์ คุณสามารถใช้คีย์เวิร์ด arguments
เพื่อสร้างดัชนีอาร์กิวเมนต์ของฟังก์ชันเพิ่มเติมได้ โปรดทราบว่าไม่มีตัวบ่งชี้สำหรับการนับอาร์กิวเมนต์
คุณสามารถใช้ Array
เพื่อแปลงตัวแปรเป็นประเภทอื่นได้ การเรียกวิธีการอินสแตนซ์ที่ไม่มีอยู่ในตัวแปรที่แคสต์ประเภทนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ คอมไพเลอร์ไม่ฉลาดพอที่จะรู้เมื่อคุณทำสิ่งนี้
/**
* Program output:
* 4
*/
class Main {
constructor Main new ( ) {
return this ;
}
function void main ( ) {
var Array a ;
var Main b ;
var String c ;
let a = Array . new ( 1 ) ;
let b = Main . new ( ) ;
let a [ 0 ] = b ;
let c = a [ 0 ] ;
// Invalidly calling `String.length` on an instance of `Main`.
do Output . printInt ( c . length ( ) ) ;
}
}
ดูโปรแกรม Overflow สำหรับตัวอย่างเชิงลึก
การปรับเปลี่ยนเฟรมสแต็กหรือรีจิสเตอร์ภายในตามลำดับซึ่งอยู่ที่ที่อยู่หน่วยความจำ 256
ถึง 2047
และ 1
ถึง 15
อาจทำให้เกิดลักษณะการทำงานที่ไม่ได้กำหนดไว้ โดยทั่วไปจะเป็นไปไม่ได้หากไม่ได้ใช้ Memory.poke
ในทางที่ผิดหรือการจัดทำดัชนีอาร์เรย์เชิงลบ ดูโปรแกรม SecretPassword สำหรับตัวอย่างเชิงลึก
NAND เสนอการตรวจสอบโปรแกรมสำหรับไฟล์ .jack
แต่ไม่ใช่สำหรับไฟล์ .vm
ซึ่งหมายความว่าคอมไพเลอร์เครื่องเสมือนของ NAND ช่วยให้คุณสามารถเรียกใช้ฟังก์ชันที่ไม่มีอยู่ อ้างอิงตัวแปรที่ไม่ได้กำหนด หรือดำเนินการกับหน่วยความจำที่ไม่ถูกต้องตามตรรกะอื่นๆ ได้อย่างอิสระ ในกรณีส่วนใหญ่ของพฤติกรรมที่ไม่ได้กำหนดไว้ เครื่องเสมือนจะหลบหนีและหน้าจอจะไม่แสดงสิ่งใดเลย มันจะเป็นความรับผิดชอบของคุณในการดีบักโปรแกรมด้วยตัวเอง
นับตั้งแต่รุ่งโรจน์ในช่วงทศวรรษ 1970 มีเหตุผลที่ดีว่าทำไมการประมวลผลแบบ 16 บิตจึงตกต่ำลงในยุคสมัยใหม่ เมื่อเปรียบเทียบกับการประมวลผลแบบ 32 บิตหรือ 64 บิต การประมวลผลแบบ 16 บิตให้พลังการประมวลผลและความจุหน่วยความจำที่จำกัด ซึ่งไม่สามารถตอบสนองความต้องการของซอฟต์แวร์และแอปพลิเคชันร่วมสมัยได้
NAND ก็ไม่มีข้อยกเว้นสำหรับความเป็นจริงนี้
นำมาจากสไลด์บรรยาย Nand ไปจนถึง Tetris
เมื่อเทียบกับ MacBook รุ่น 16 GiB ของคุณ NAND มี RAM เพียง 4 KiB เพียงเล็กน้อย ซึ่งมีอัตราส่วน 0.00002% ! แม้ว่าจะเป็นเช่นนี้ มันก็เพียงพอแล้วที่จะพาเราไปดวงจันทร์ ดังนั้นใครจะบอกว่า NAND ก็ทำไม่ได้เช่นกัน
Jack OS สำรองหน่วยความจำ 14,336 ที่อยู่